/*-------------------------------------------------------------------------
 *
 * nbtinsert.c
 *	  在 Postgres 的 Lehman 和 Yao btrees 中插入项目。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/access/nbtree/nbtinsert.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/nbtree.h"
#include "access/nbtxlog.h"
#include "access/transam.h"
#include "access/xloginsert.h"
#include "common/pg_prng.h"
#include "lib/qunique.h"
#include "miscadmin.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
#include "storage/smgr.h"

/* 对于快速路径优化的最小树高度 */
#define BTREE_FASTPATH_MIN_LEVEL	2


static BTStack fc__bt_search_insert(Relation fc_rel, BTInsertState fc_insertstate);
static TransactionId fc__bt_check_unique(Relation fc_rel, BTInsertState fc_insertstate,
									  Relation fc_heapRel,
									  IndexUniqueCheck fc_checkUnique, bool *fc_is_unique,
									  uint32 *fc_speculativeToken);
static OffsetNumber fc__bt_findinsertloc(Relation fc_rel,
									  BTInsertState fc_insertstate,
									  bool fc_checkingunique,
									  bool fc_indexUnchanged,
									  BTStack fc_stack,
									  Relation fc_heapRel);
static void fc__bt_stepright(Relation fc_rel, BTInsertState fc_insertstate, BTStack fc_stack);
static void fc__bt_insertonpg(Relation fc_rel, BTScanInsert fc_itup_key,
						   Buffer fc_buf,
						   Buffer fc_cbuf,
						   BTStack fc_stack,
						   IndexTuple fc_itup,
						   Size fc_itemsz,
						   OffsetNumber fc_newitemoff,
						   int fc_postingoff,
						   bool fc_split_only_page);
static Buffer fc__bt_split(Relation fc_rel, BTScanInsert fc_itup_key, Buffer fc_buf,
						Buffer fc_cbuf, OffsetNumber fc_newitemoff, Size fc_newitemsz,
						IndexTuple fc_newitem, IndexTuple fc_orignewitem,
						IndexTuple fc_nposting, uint16 fc_postingoff);
static void fc__bt_insert_parent(Relation fc_rel, Buffer fc_buf, Buffer fc_rbuf,
							  BTStack fc_stack, bool fc_isroot, bool fc_isonly);
static Buffer fc__bt_newroot(Relation fc_rel, Buffer fc_lbuf, Buffer fc_rbuf);
static inline bool fc__bt_pgaddtup(Page fc_page, Size fc_itemsize, IndexTuple fc_itup,
								OffsetNumber fc_itup_off, bool fc_newfirstdataitem);
static void fc__bt_delete_or_dedup_one_page(Relation fc_rel, Relation fc_heapRel,
										 BTInsertState fc_insertstate,
										 bool fc_simpleonly, bool fc_checkingunique,
										 bool fc_uniquedup, bool fc_indexUnchanged);
static void fc__bt_simpledel_pass(Relation fc_rel, Buffer fc_buffer, Relation fc_heapRel,
							   OffsetNumber *fc_deletable, int fc_ndeletable,
							   IndexTuple fc_newitem, OffsetNumber fc_minoff,
							   OffsetNumber fc_maxoff);
static BlockNumber *fc__bt_deadblocks(Page fc_page, OffsetNumber *fc_deletable,
								   int fc_ndeletable, IndexTuple fc_newitem,
								   int *fc_nblocks);
static inline int fc__bt_blk_cmp(const void *fc_arg1, const void *fc_arg2);

/*
 *	_bt_doinsert() -- 处理在树中插入单个索引元组。
 *
 *		该例程由公共接口例程btinsert调用。
 *		到这里时，itup已经填写，包括TID。
 *
 *		如果checkUnique为UNIQUE_CHECK_NO或UNIQUE_CHECK_PARTIAL，则
 *		这将允许重复。否则（UNIQUE_CHECK_YES或
 *		UNIQUE_CHECK_EXISTING）将会对重复抛出错误。
 *		对于UNIQUE_CHECK_EXISTING，我们仅仅运行重复检查，而
 *		不实际进行插入。
 *
 *		indexUnchanged执行器提示表示if itup来自于
 *		没有逻辑改变索引值的UPDATE，但必须依然有一个新条目来指向后继
 *		版本。
 *
 *		结果值仅对UNIQUE_CHECK_PARTIAL有意义：
 *		如果条目已知唯一，则必须为真，否则为假。
 *		（在当前实现中，我们在成功的UNIQUE_CHECK_YES或
 *		UNIQUE_CHECK_EXISTING调用后也会返回true，但
 *		这仅仅是一个编码副作用。）
 */
bool _bt_doinsert(Relation fc_rel, IndexTuple fc_itup,
			 IndexUniqueCheck fc_checkUnique, bool fc_indexUnchanged,
			 Relation fc_heapRel)
{
	bool		fc_is_unique = false;
	BTInsertStateData fc_insertstate;
	BTScanInsert fc_itup_key;
	BTStack		fc_stack;
	bool		fc_checkingunique = (fc_checkUnique != UNIQUE_CHECK_NO);

	/* 我们需要一个插入扫描键来进行搜索，因此构建一个 */
	fc_itup_key = _bt_mkscankey(fc_rel, fc_itup);

	if (fc_checkingunique)
	{
		if (!fc_itup_key->anynullkeys)
		{
			/* 在建立唯一性之前没有（heapkeyspace）scantid */
			fc_itup_key->scantid = NULL;
		}
		else
		{
			/*
			 * 新元组的扫描键包含NULL键值。绕过
			 * checkingunique步骤。它们是多余的，因为核心代码
			 * 认为NULL与每个值（包括NULL）不相等。
			 *
			 * 该优化避免了在
			 * _bt_findinsertloc() heapkeyspace路径中的O(N^2)行为，当一个唯一索引有
			 * 大量带NULL键值的“重复”时。
			 */
			fc_checkingunique = false;
			/* 元组在核心代码关心的意义上是唯一的 */
			Assert(fc_checkUnique != UNIQUE_CHECK_EXISTING);
			fc_is_unique = true;
		}
	}

	/*
	 * 填充BTInsertState工作区，以跟踪当前页面和
	 * 要插入页面内的位置。
	 *
	 * 请注意，itemsz被传递给处理
	 * 插入项目的较低级别代码。它必须进行MAXALIGN()对齐。
	 * 这确保了空间
	 * 统计代码始终考虑我们
	 * 期望PageAddItem()稍后会增加的对齐开销。
	 * （实际上，index_form_tuple()在对齐方面已经是保守的，
	 * 但我们不依赖于这一点。此外，出于nbtsplitloc.c的利益，保留“真实”的元组大小
	 * 在索引元组头中可能会在某一天发生。
	 * 请注意，heapam并不会对每个堆元组的lp_len字段进行MAXALIGN()对齐。）
	 */
	fc_insertstate.itup = fc_itup;
	fc_insertstate.itemsz = MAXALIGN(IndexTupleSize(fc_itup));
	fc_insertstate.itup_key = fc_itup_key;
	fc_insertstate.bounds_valid = false;
	fc_insertstate.buf = InvalidBuffer;
	fc_insertstate.postingoff = 0;

search:

	/*
	 * 通过从根页面搜索找到并锁定应添加元组的叶页面。
	 * insertstate.buf将持有一个稍后以排他模式锁定的缓冲区。
	 */
	fc_stack = fc__bt_search_insert(fc_rel, &fc_insertstate);

	/*
	 * 检查唯一性插入是不允许进行的，当两个元组具有相等的键属性值并且在事务提交后将对新的MVCC快照可见时。这里需要检查锁定页面/缓冲区中的冲突（如果需要）。
	 *
	 * 可能需要在_bt_check_unique中检查右侧的页面，尽管这种情况应该非常少见。实际上，值可能所在的第一个页面（省略scantid的情况下）几乎总是也是能够找到匹配元组的唯一页面。这是因为_bt_findsplitloc在处理重复元组时的行为——当没有候选叶页面分裂点可以避免时，一组重复的元组只能被允许跨越页面边界。此外，_bt_check_unique可以使用叶页面高键来确定右侧兄弟节点不会有重复项，而不必实际访问它（在新项恰好位于叶页面最右侧的情况下，它会使用高键）。
	 *
	 * 注意：显然，_bt_check_unique只能检测到已经存在于索引中的键；因此，它无法防御相同键的并发插入。我们通过对值可能所在的第一页持有写锁来保护这一点，隐式堆TID平局属性使用省略/-inf值。任何其他想要插入相同键的事务必须在同一页面上获取写锁，因此只有一个潜在的插入者可以在任意时刻进行检查。此外，一旦我们通过了检查，我们会持续持有写锁，直到完成插入，这样后续的插入者就无法未能看到我们的插入。（这需要在_bt_findinsertloc中格外小心。）
	 *
	 * 如果我们必须等待另一个事务，我们会在等待时释放锁，然后必须进行新的搜索。
	 *
	 * 对于部分唯一性检查，我们不等待其他事务。只需让元组插入并返回可能不唯一的false，或返回绝对唯一的true。
	 */
	if (fc_checkingunique)
	{
		TransactionId fc_xwait;
		uint32		fc_speculativeToken;

		fc_xwait = fc__bt_check_unique(fc_rel, &fc_insertstate, fc_heapRel, fc_checkUnique,
								 &fc_is_unique, &fc_speculativeToken);

		if (unlikely(TransactionIdIsValid(fc_xwait)))
		{
			/* 必须等待另一个家伙... */
			_bt_relbuf(fc_rel, fc_insertstate.buf);
			fc_insertstate.buf = InvalidBuffer;

			/*
			 * 如果这是一次推测性插入，等待其完成（即，继续进行插入，或终止元组）。否则，像往常一样等待事务完成。
			 */
			if (fc_speculativeToken)
				SpeculativeInsertionWait(fc_xwait, fc_speculativeToken);
			else
				XactLockTableWait(fc_xwait, fc_rel, &fc_itup->t_tid, XLTW_InsertIndex);

			/* 从头开始... */
			if (fc_stack)
				_bt_freestack(fc_stack);
			goto search;
		}

		/* 唯一性已经建立 -- 恢复堆tid为scantid */
		if (fc_itup_key->heapkeyspace)
			fc_itup_key->scantid = &fc_itup->t_tid;
	}

	if (fc_checkUnique != UNIQUE_CHECK_EXISTING)
	{
		OffsetNumber fc_newitemoff;

		/*
		 * 唯一性冲突谓词锁定只关心索引中的插入冲突与现有锁的情况。我们尚不确定在checkingunique和!heapkeyspace情况下将要插入的实际页面，但使用值可能所在的第一页（省略scantid的情况下）是可以的。
		 */
		CheckForSerializableConflictIn(fc_rel, NULL, BufferGetBlockNumber(fc_insertstate.buf));

		/*
		 * 执行插入。注意，insertstate包含在_bt_check_unique中建立的缓存的二叉搜索边界，当插入正在进行唯一性检查时。
		 */
		fc_newitemoff = fc__bt_findinsertloc(fc_rel, &fc_insertstate, fc_checkingunique,
									   fc_indexUnchanged, fc_stack, fc_heapRel);
		fc__bt_insertonpg(fc_rel, fc_itup_key, fc_insertstate.buf, InvalidBuffer, fc_stack,
					   fc_itup, fc_insertstate.itemsz, fc_newitemoff,
					   fc_insertstate.postingoff, false);
	}
	else
	{
		/* 只需释放缓冲区 */
		_bt_relbuf(fc_rel, fc_insertstate.buf);
	}

	/* 保持整洁 */
	if (fc_stack)
		_bt_freestack(fc_stack);
	pfree(fc_itup_key);

	return fc_is_unique;
}

/*
 *	_bt_search_insert() -- _bt_search() 插入的包装
 *
 * 在树中搜索特定的 scankey，或者更确切地说，是寻找它可能在的第一个
 * 叶页。尽量在实际从根页开始搜索树之前，利用快速路径优化的
 * 右侧叶页缓存。
 *
 * 返回值是一个父页指针的栈（不过请查看下面关于快速路径优化和
 * 页拆分的说明）。insertstate->buf 被设置为叶页缓冲区的地址，
 * 无论如何都被写锁定和固定（如果需要，为调用者创建一个新的
 * 空根页）。
 *
 * 快速路径优化避免了在单个后台在索引的最右侧叶页上连续插入
 * 新元组时反复搜索树的大部分工作。右侧叶页的后台缓存是在
 * _bt_insertonpg() 中维护的，并在这里使用。当必须在非最右侧叶页
 * 上插入非支点元组时，这个缓存会在这里失效。
 *
 * 此优化对自增字段的索引有帮助。它还帮助处理日期时间列的索引，
 * 以及存在大量 NULL 值的索引。（对于单列索引，NULL 通常被插入到
 * 最右页，因为它们通常被认为在键空间中的其他一切之后。由于堆
 * TID 列的影响，单个 NULL 元组一般会被放置在最右侧叶页上。）
 *
 * 注意，当最右页上没有足够的空间容纳调用者的新条目时，我们会
 * 避免应用该优化。这是必要的，因为当预计会发生页拆分时，我们
 * 需要返回一个真实的下降栈（实际上，调用者可以处理使用 NULL
 * 栈的叶页拆分，但那非常慢，因此必须避免）。还请注意，当在右侧
 * 页上存在并发插入时，快速路径优化会有条件地获取该页上的锁，
 * 以降低额外的竞争（如果必须等待锁，我们就放弃）。我们假设当
 * 存在竞争时，应用此优化没有用，因为每个后台缓存不会长时间有效。
 */
static BTStack
fc__bt_search_insert(Relation fc_rel, BTInsertState fc_insertstate)
{
	Assert(fc_insertstate->buf == InvalidBuffer);
	Assert(!fc_insertstate->bounds_valid);
	Assert(fc_insertstate->postingoff == 0);

	if (RelationGetTargetBlock(fc_rel) != InvalidBlockNumber)
	{
		/* 模拟带条件锁定的 _bt_getbuf() 调用 */
		fc_insertstate->buf = ReadBuffer(fc_rel, RelationGetTargetBlock(fc_rel));
		if (_bt_conditionallockbuf(fc_rel, fc_insertstate->buf))
		{
			Page		fc_page;
			BTPageOpaque fc_opaque;

			_bt_checkpage(fc_rel, fc_insertstate->buf);
			fc_page = BufferGetPage(fc_insertstate->buf);
			fc_opaque = BTPageGetOpaque(fc_page);

			/*
			 * 检查该页是否仍然是最右叶页，并且是否有足够的空
			 * 间容纳新的元组。还要检查插入扫描键是否严格大于
			 * 页上第一个非支点元组。（请注意，当我们的调用者是
			 * checkingunique 插入者时，我们期望它up_key 的
			 * scantid 是未设置的。）
			 */
			if (P_RIGHTMOST(fc_opaque) &&
				P_ISLEAF(fc_opaque) &&
				!P_IGNORE(fc_opaque) &&
				PageGetFreeSpace(fc_page) > fc_insertstate->itemsz &&
				PageGetMaxOffsetNumber(fc_page) >= P_HIKEY &&
				_bt_compare(fc_rel, fc_insertstate->itup_key, fc_page, P_HIKEY) > 0)
			{
				/*
 * 调用者可以使用快速路径优化，因为缓存的
 * 块仍然是最右侧的叶页，可以容纳调用者的
 * 新元组而无需拆分。将块保留在本地缓存中以便
 * 进行下一次插入，并让调用者使用NULL栈。
 *
 * 请注意，_bt_insert_parent()有一个断言来捕获
 * 从快速路径插入中演变而来的叶页拆分
 * （它应该仅在需要处理并发根页拆分时传递
 * NULL栈，而绝不能因为这里返回了NULL栈而传递）。
 */
				return NULL;
			}

			/* 页面不适合调用者，释放锁和固定 */
			_bt_relbuf(fc_rel, fc_insertstate->buf);
		}
		else
		{
			/* 锁不可用，释放固定 */
			ReleaseBuffer(fc_insertstate->buf);
		}

		/* 忘记块，因为缓存似乎无用 */
		RelationSetTargetBlock(fc_rel, InvalidBlockNumber);
	}

	/* 无法使用优化 -- 下降树，返回适当的下降栈 */
	return _bt_search(fc_rel, fc_insertstate->itup_key, &fc_insertstate->buf, BT_WRITE,
					  NULL);
}

/*
 *	_bt_check_unique() -- 检查唯一索引约束的违规情况
 *
 * 如果没有冲突，则返回InvalidTransactionId，否则返回一个我们
 * 必须等待以查看是否提交了冲突元组的xact ID。如果检测到实际
 * 冲突，则不返回 --- 只需ereport()。如果返回了xact ID，并且
 * 冲突元组仍在进行投机性插入，则*speculativeToken被设置为非零，
 * 调用者可以使用SpeculativeInsertionWait()等待插入的裁定。
 *
 * 然而，如果checkUnique == UNIQUE_CHECK_PARTIAL，我们总是返回
 * InvalidTransactionId，因为我们不想等待。在这种情况下，如果
 * 存在潜在冲突，我们将*is_unique设置为false，核心代码必须在
 * 稍后重新进行唯一性检查。
 *
 * 作为副作用，在insertstate中设置状态，稍后可以被
 * _bt_findinsertloc()使用，以重用我们在这里进行的大部分二分搜索工作。
 *
 * 该代码将NULL视为相等，这与唯一索引的默认语义不同。
 * 因此，当扫描键中存在NULL值且索引使用默认的NULLS DISTINCT
 * 模式时，请勿在此调用。
 */
static TransactionId fc__bt_check_unique(Relation fc_rel, BTInsertState fc_insertstate, Relation fc_heapRel,
				 IndexUniqueCheck fc_checkUnique, bool *fc_is_unique,
				 uint32 *fc_speculativeToken)
{
	IndexTuple	fc_itup = fc_insertstate->itup;
	IndexTuple	fc_curitup = NULL;
	ItemId		fc_curitemid = NULL;
	BTScanInsert fc_itup_key = fc_insertstate->itup_key;
	SnapshotData fc_SnapshotDirty;
	OffsetNumber fc_offset;
	OffsetNumber fc_maxoff;
	Page		fc_page;
	BTPageOpaque fc_opaque;
	Buffer		fc_nbuf = InvalidBuffer;
	bool		fc_found = false;
	bool		fc_inposting = false;
	bool		fc_prevalldead = true;
	int			fc_curposti = 0;

	/* 假设唯一，直到找到重复项 */
	*fc_is_unique = true;

	InitDirtySnapshot(fc_SnapshotDirty);

	fc_page = BufferGetPage(fc_insertstate->buf);
	fc_opaque = BTPageGetOpaque(fc_page);
	fc_maxoff = PageGetMaxOffsetNumber(fc_page);

	/*
	 * 找到第一个具有相同键的元组。
	 *
	 * 这也将二分搜索边界保存在insertstate中。我们将在下面的快速
	 * 路径中使用它们，但也会在稍后的_bt_findinsertloc()调用中使用。
	 */
	Assert(!fc_insertstate->bounds_valid);
	fc_offset = _bt_binsrch_insert(fc_rel, fc_insertstate);

	/*
	 * 扫描所有相等的元组，寻找活跃冲突。
	 */
	Assert(!fc_insertstate->bounds_valid || fc_insertstate->low == fc_offset);
	Assert(!fc_itup_key->anynullkeys);
	Assert(fc_itup_key->scantid == NULL);
	for (;;)
	{
		/*
		 * 循环的每次迭代处理一个堆 TID，而不是一个索引元组。
		 * 当前页面的偏移量通常不会在处理来自发布列表元组的堆 TIDs 的迭代中推进。
		 *
		 * "inposting" 状态在 _inside_ 发布列表时设置 --- 而不是在
		 * 发布列表的开始（或结束）时。我们在迭代的末尾推进 curposti
		 * 当进入一个发布列表元组时。一般而言，循环的每次迭代要么推进页面偏移量要么
		 * 推进 curposti --- 处理发布列表中最右侧/最大堆 TID 的迭代最终推进
		 * 页面偏移量（并取消设置 "inposting"）。
		 *
		 * 在尝试检查之前，请确保偏移量指向实际的索引元组...
		 */
		if (fc_offset <= fc_maxoff)
		{
			/*
			 * 快捷路径：在大多数情况下，我们可以使用缓存的搜索边界
			 * 将我们的考虑限制为肯定是重复的项目。 当原始页面为空，
			 * 或初始偏移量超出原始页面的末尾时，此快捷路径不适用，
			 * 这可能表明我们需要检查第二个或后续页面。
			 *
			 * 请注意，这种优化使我们能够避免在没有重复项时直接调用
			 * _bt_compare()，只要关键字将要放置的位置不在页面的末尾。
			 */
			if (fc_nbuf == InvalidBuffer && fc_offset == fc_insertstate->stricthigh)
			{
				Assert(fc_insertstate->bounds_valid);
				Assert(fc_insertstate->low >= P_FIRSTDATAKEY(fc_opaque));
				Assert(fc_insertstate->low <= fc_insertstate->stricthigh);
				Assert(_bt_compare(fc_rel, fc_itup_key, fc_page, fc_offset) < 0);
				break;
			}

			/*
			 * 我们可以跳过已标记为已删除的项目。
			 *
			 * 在大量更新活动的情况下，索引可能包含多个具有相同键的
			 * 被删除项目；在每个被删除项目上运行 _bt_compare() 变得
			 * 成本高昂。我们只是尽可能快速地跳过被删除的项目。
			 * 只有在我们达到一个未被删除的项目时，我们才应用 _bt_compare()。
			 * 我们可以重用边界，以避免对已知的相等元组进行 _bt_compare() 调用，
			 * 但是这似乎不值得。
			 */
			if (!fc_inposting)
				fc_curitemid = PageGetItemId(fc_page, fc_offset);
			if (fc_inposting || !ItemIdIsDead(fc_curitemid))
			{
				ItemPointerData fc_htid;
				bool		fc_all_dead = false;

				if (!fc_inposting)
				{
					/* 普通元组，或发布列表元组中的第一个 TID */
					if (_bt_compare(fc_rel, fc_itup_key, fc_page, fc_offset) != 0)
						break;	/* 我们已经超越了所有相等的元组 */

					/* 推进 curitup */
					fc_curitup = (IndexTuple) PageGetItem(fc_page, fc_curitemid);
					Assert(!BTreeTupleIsPivot(fc_curitup));
				}

				/* 好吧，我们得用 htid 来获取堆元组... */
				if (!BTreeTupleIsPosting(fc_curitup))
				{
					/* ... htid 来自简单的非支点元组 */
					Assert(!fc_inposting);
					fc_htid = fc_curitup->t_tid;
				}
				else if (!fc_inposting)
				{
					/* ... htid 是新发布列表中的第一个 TID */
					fc_inposting = true;
					fc_prevalldead = true;
					fc_curposti = 0;
					fc_htid = *BTreeTupleGetPostingN(fc_curitup, 0);
				}
				else
				{
					/* ... htid 是发布列表中的第二个或后续 TID */
					Assert(fc_curposti > 0);
					fc_htid = *BTreeTupleGetPostingN(fc_curitup, fc_curposti);
				}

				/*
				 * 如果我们正在进行复查，我们期望找到我们正在复查的元组。
				 * 它不是重复项，但我们必须继续扫描。
				 */
				if (fc_checkUnique == UNIQUE_CHECK_EXISTING &&
					ItemPointerCompare(&fc_htid, &fc_itup->t_tid) == 0)
				{
					fc_found = true;
				}

				/*
				 * 检查是否有任何表元组满足此索引条目的 SnapshotDirty。
				 * 这是必要的，因为对于具有像堆的 HOT 这样的优化的 AM，
				 * 我们只为整个链有一个索引条目。
				 */
				else if (table_index_fetch_tuple_check(fc_heapRel, &fc_htid,
													   &fc_SnapshotDirty,
													   &fc_all_dead))
				{
					TransactionId fc_xwait;

					/*
					 * 它是一个重复项。如果我们只是进行部分检查，
					 * 那么就不用检查这个元组是否正在另一个事务中被更新。
					 * 只需返回它是一个潜在冲突的事实，并将完整检查留到稍后。
					 * 不要使二分搜索边界无效。
					 */
					if (fc_checkUnique == UNIQUE_CHECK_PARTIAL)
					{
						if (fc_nbuf != InvalidBuffer)
							_bt_relbuf(fc_rel, fc_nbuf);
						*fc_is_unique = false;
						return InvalidTransactionId;
					}

					/*
					 * 如果这个元组正在被其他事务更新
					 * 那么我们必须等待它的提交/中止。
					 */
					fc_xwait = (TransactionIdIsValid(fc_SnapshotDirty.xmin)) ?
						fc_SnapshotDirty.xmin : fc_SnapshotDirty.xmax;

					if (TransactionIdIsValid(fc_xwait))
					{
						if (fc_nbuf != InvalidBuffer)
							_bt_relbuf(fc_rel, fc_nbuf);
						/* 告诉 _bt_doinsert 等待... */
						*fc_speculativeToken = fc_SnapshotDirty.speculativeToken;
						/* 调用者立即释放对缓冲区的锁 */
						fc_insertstate->bounds_valid = false;
						return fc_xwait;
					}

					/*
					 * 否则我们有一个明确的冲突。但在
					 * 抱怨之前，查看我们想要插入的元组
					 * 是否现在已提交为死 --- 如果是，就不要抱怨。
					 * 在正常场景中这是浪费时间，但我们必须
					 * 这样做以支持 CREATE INDEX CONCURRENTLY。
					 *
					 * 我们必须遵循 HOT 链，因为在
					 * 并发索引构建期间，我们插入根 TID，尽管
					 * 实际元组可能在 HOT 链的某处。
					 * 在跟随链时，我们可能不会停在触发插入的
					 * 确切元组上，但这没关系，因为如果我们在
					 * 此链中的任何位置找到一个活动元组，
					 * 我们就有一个唯一键冲突。其他活动元组不是
					 * 此链的一部分，因为它有一个不同的索引
					 * 条目。
					 */
					fc_htid = fc_itup->t_tid;
					if (table_index_fetch_tuple_check(fc_heapRel, &fc_htid,
													  SnapshotSelf, NULL))
					{
						/* 正常情况 --- 它仍然存在 */
					}
					else
					{
						/*
						 * 它已经被删除，因此没有错误，也不需要
						 * 继续搜索
						 */
						break;
					}

					/*
					 * 检查冲突，就像我们打算
					 * 写入此页面一样。我们实际上并不打算写入，
					 * 但我们希望有机会报告 SSI 冲突，这会
					 * 被这个唯一约束
					 * 违反所掩盖。
					 */
					CheckForSerializableConflictIn(fc_rel, NULL, BufferGetBlockNumber(fc_insertstate->buf));

					/*
					 * 这是一个明确的冲突。将元组拆分为
					 * 数据项并报告错误。但首先，确保我们
					 * 释放我们持有的缓冲区锁 ---
					 * BuildIndexValueDescription 可能进行目录访问，
					 * 在最坏的情况下可能会触及同一索引并
					 * 导致死锁。
					 */
					if (fc_nbuf != InvalidBuffer)
						_bt_relbuf(fc_rel, fc_nbuf);
					_bt_relbuf(fc_rel, fc_insertstate->buf);
					fc_insertstate->buf = InvalidBuffer;
					fc_insertstate->bounds_valid = false;

					{
						Datum		fc_values[INDEX_MAX_KEYS];
						bool		fc_isnull[INDEX_MAX_KEYS];
						char	   *fc_key_desc;

						index_deform_tuple(fc_itup, RelationGetDescr(fc_rel),
										   fc_values, fc_isnull);

						fc_key_desc = BuildIndexValueDescription(fc_rel, fc_values,
															  fc_isnull);

						ereport(ERROR,
								(errcode(ERRCODE_UNIQUE_VIOLATION),
								 errmsg("duplicate key value violates unique constraint \"%s\"",
										RelationGetRelationName(fc_rel)),
								 fc_key_desc ? errdetail("Key %s already exists.",
													  fc_key_desc) : 0,
								 errtableconstraint(fc_heapRel,
													RelationGetRelationName(fc_rel))));
					}
				}
				else if (fc_all_dead && (!fc_inposting ||
									  (fc_prevalldead &&
									   fc_curposti == BTreeTupleGetNPosting(fc_curitup) - 1)))
				{
					/*
					 * 冲突的元组（或所有热点链指向的
					 * 所有发布列表 TID）对所有人都是死的，因此标记
					 * 索引条目为已删除。
					 */
					ItemIdMarkDead(fc_curitemid);
					fc_opaque->btpo_flags |= BTP_HAS_GARBAGE;

					/*
					 * 用脏提示标记缓冲区，因为状态不是
					 * 关键。确保标记正确的缓冲区为脏。
					 */
					if (fc_nbuf != InvalidBuffer)
						MarkBufferDirtyHint(fc_nbuf, true);
					else
						MarkBufferDirtyHint(fc_insertstate->buf, true);
				}

				/*
				 * 记住，如果发布列表元组至少有一个 HOT 链
				 * 其成员不都是死的
				 */
				if (!fc_all_dead && fc_inposting)
					fc_prevalldead = false;
			}
		}

		if (fc_inposting && fc_curposti < BTreeTupleGetNPosting(fc_curitup) - 1)
		{
			/* 前进到同一发布列表中的下一个 TID */
			fc_curposti++;
			continue;
		}
		else if (fc_offset < fc_maxoff)
		{
			/* 前进到下一个元组 */
			fc_curposti = 0;
			fc_inposting = false;
			fc_offset = OffsetNumberNext(fc_offset);
		}
		else
		{
			int			fc_highkeycmp;

			/* 如果 scankey == hikey 我们还需要检查下一页 */
			if (P_RIGHTMOST(fc_opaque))
				break;
			fc_highkeycmp = _bt_compare(fc_rel, fc_itup_key, fc_page, P_HIKEY);
			Assert(fc_highkeycmp <= 0);
			if (fc_highkeycmp != 0)
				break;
			/* 前进到下一个非死页 --- 一定有一个 */
			for (;;)
			{
				BlockNumber fc_nblkno = fc_opaque->btpo_next;

				fc_nbuf = _bt_relandgetbuf(fc_rel, fc_nbuf, fc_nblkno, BT_READ);
				fc_page = BufferGetPage(fc_nbuf);
				fc_opaque = BTPageGetOpaque(fc_page);
				if (!P_IGNORE(fc_opaque))
					break;
				if (P_RIGHTMOST(fc_opaque))
					elog(ERROR, "fell off the end of index \"%s\"",
						 RelationGetRelationName(fc_rel));
			}
			/* 也将前进到下一个元组 */
			fc_curposti = 0;
			fc_inposting = false;
			fc_maxoff = PageGetMaxOffsetNumber(fc_page);
			fc_offset = P_FIRSTDATAKEY(fc_opaque);
			/* 不要使二分搜索边界失效 */
		}
	}

	/*
	 * 如果我们正在进行重新检查，那么我们应该已经找到了要
	 * 检查的元组。否则，有些地方非常错误 --- 很可能，
	 * 索引是基于非不变表达式。
	 */
	if (fc_checkUnique == UNIQUE_CHECK_EXISTING && !fc_found)
		ereport(ERROR,
				(errcode(ERRCODE_INTERNAL_ERROR),
				 errmsg("failed to re-find tuple within index \"%s\"",
						RelationGetRelationName(fc_rel)),
				 errhint("This may be because of a non-immutable index expression."),
				 errtableconstraint(fc_heapRel,
									RelationGetRelationName(fc_rel))));

	if (fc_nbuf != InvalidBuffer)
		_bt_relbuf(fc_rel, fc_nbuf);

	return InvalidTransactionId;
}


/*
 *	_bt_findinsertloc() -- 为一个元组找到插入位置
 *
 *		在入口时，insertstate 缓冲区包含新元组所在的页面。
 *		它被调用者独占锁定并固定。
 *
 *		如果 'checkingunique' 为真，入口时的缓冲区是第一个包含新键
 *		重复值的页面。如果有多个页面上存在重复值，正确的
 *		插入位置可能在右侧的一些页面上，而不是第一个页面。
 *		在这种情况下，此函数会向右移动到正确的目标页面。
 *
 *		（在一个 !heapkeyspace 索引中，可能有多个页面具有相同的
 *		高键，新元组可以合法地放置在这些页面上。在这种情况下，
 *		调用者传递第一个包含重复值的页面，就像当 checkingunique=true
 *		时一样。如果该页面没有足够的空间容纳新元组，此函数会
 *		向右移动，尝试找到一个合法的页面来放置它。）
 *
 *		如果 'indexUnchanged' 为真，这表示一个没有从逻辑上改变
 *		索引值的 UPDATE，但仍然必须有一个新的条目指向
 *		后续版本。来自执行器的这个提示将影响我们在页面需要
 *		被分割时的行为，我们必须考虑我们的选项。自底向上的
 *		索引删除可以避免病态的版本驱动页面分割，但我们只会在
 *		对其适用性有适度信心时才会努力尝试。这个提示不会显著
 *		影响我们的行为，除非几乎所有对叶子页面的插入都得到
 *		这个提示。
 *
 *		在退出时，insertstate 缓冲区包含选择的插入页面，并且
 *		返回该页面内的偏移量。如果 _bt_findinsertloc 需要
 *		向右移动，则释放原页面上的锁和固定，并将新缓冲区
 *		独占锁定并固定。
 *
 *		如果 insertstate 包含缓存的二分搜索边界，我们将利用
 *		它们。这避免了重复在 _bt_check_unique() 中已经做过的
 *		比较。
 */
static OffsetNumber fc__bt_findinsertloc(Relation fc_rel,
				  BTInsertState fc_insertstate,
				  bool fc_checkingunique,
				  bool fc_indexUnchanged,
				  BTStack fc_stack,
				  Relation fc_heapRel)
{
	BTScanInsert fc_itup_key = fc_insertstate->itup_key;
	Page		fc_page = BufferGetPage(fc_insertstate->buf);
	BTPageOpaque fc_opaque;
	OffsetNumber fc_newitemoff;

	fc_opaque = BTPageGetOpaque(fc_page);

	/* 检查 1/3 页的限制 */
	if (unlikely(fc_insertstate->itemsz > BTMaxItemSize(fc_page)))
		_bt_check_third_page(fc_rel, fc_heapRel, fc_itup_key->heapkeyspace, fc_page,
							 fc_insertstate->itup);

	Assert(P_ISLEAF(fc_opaque) && !P_INCOMPLETE_SPLIT(fc_opaque));
	Assert(!fc_insertstate->bounds_valid || fc_checkingunique);
	Assert(!fc_itup_key->heapkeyspace || fc_itup_key->scantid != NULL);
	Assert(fc_itup_key->heapkeyspace || fc_itup_key->scantid == NULL);
	Assert(!fc_itup_key->allequalimage || fc_itup_key->heapkeyspace);

	if (fc_itup_key->heapkeyspace)
	{
		/* 跟踪是否见到 checkingunique 的重复值 */
		bool		fc_uniquedup = fc_indexUnchanged;

		/*
		 * 如果我们正在向一个唯一索引插入，可能需要向右遍历
		 * 叶子页面，以找到必须插入的那一个叶子页面。
		 *
		 * 这对于 checkingunique 调用者是必要的，因为在调用
		 * _bt_search() 时未使用 scantid。scantid 只能在
		 * _bt_check_unique() 检查了重复值之后设置。最初存储在
		 * insertstate->buf 中的缓冲区包含可能找到第一个重复键
		 * 的页面，这并不总是新元组应放置的页面。新元组的堆 TID
		 * 属性（scantid）可能强制我们在一个兄弟页面上插入，
		 * 尽管这种情况在实践中应该非常少见。
		 */
		if (fc_checkingunique)
		{
			if (fc_insertstate->low < fc_insertstate->stricthigh)
			{
				/* 在 _bt_check_unique() 中遇到重复值 */
				Assert(fc_insertstate->bounds_valid);
				fc_uniquedup = true;
			}

			for (;;)
			{
				/*
				 * 这个新元组是否属于这一页？
				 *
				 * 之前的 _bt_check_unique() 调用可能已经为新项目建立了严格的上限偏移量。
				 * 如果它不是页面的最后一个项目（即，如果页面上至少有一个元组在我们插入的元组之后），
				 * 那么我们知道这个元组属于这一页。我们可以跳过高关键字检查。
				 */
				if (fc_insertstate->bounds_valid &&
					fc_insertstate->low <= fc_insertstate->stricthigh &&
					fc_insertstate->stricthigh <= PageGetMaxOffsetNumber(fc_page))
					break;

				/* 测试 '<='，而不是 '!='，因为 scantid 现在已经设置 */
				if (P_RIGHTMOST(fc_opaque) ||
					_bt_compare(fc_rel, fc_itup_key, fc_page, P_HIKEY) <= 0)
					break;

				fc__bt_stepright(fc_rel, fc_insertstate, fc_stack);
				/* 在向右移动后更新本地状态 */
				fc_page = BufferGetPage(fc_insertstate->buf);
				fc_opaque = BTPageGetOpaque(fc_page);
				/* 假设存在重复项（如果正在检查唯一性） */
				fc_uniquedup = true;
			}
		}

		/*
		 * 如果目标页面无法容纳新项目，则尝试通过现在执行删除或去重来避免在插入时拆分页面
		 */
		if (PageGetFreeSpace(fc_page) < fc_insertstate->itemsz)
			fc__bt_delete_or_dedup_one_page(fc_rel, fc_heapRel, fc_insertstate, false,
										 fc_checkingunique, fc_uniquedup,
										 fc_indexUnchanged);
	}
	else
	{
		/*---------- 
		 * 这是一个 !heapkeyspace（版本 2 或 3）索引。当前页面是我们可以插入新元组的第一页，
		 * 但可能在右侧还有其他页面可以选择使用。
		 *
		 * 如果新关键字等于一个或多个现有关键字，我们可以合法地将其放置在相等关键字系列的任何位置。
		 * 事实上，如果新关键字等于页面的“高关键字”，我们可以将其放在下一页。
		 * 如果它等于高关键字，并且在当前页面上没有足够的空间插入新元组而不拆分，那么我们向右移动，
		 * 希望找到更多的空闲空间，避免分页拆分。
		 *
		 * 继续向右扫描，直到我们
		 *		(a) 找到一个有足够空闲空间的页面，
		 *		(b) 到达元组可以合法存在的最后一页，或
		 *		(c) 感到疲惫不堪。
		 * (c) 不是随便说说；这很重要，因为如果有许多页面的相同关键字，拆分较早的页面会比在每次插入时
		 * 扫描到相等关键字的末尾更好。我们将“感到疲惫”实现为随机选择，
		 * 因为在扫描固定数量的页面后停止效果不好（我们永远无法到达以前拆分页面的右侧）。
		 * 向右移动的概率设置为 0.99，这可能看起来太高而不会改变行为，但它确实很好地防止了
		 * 拥有许多相同关键字的 O(N^2) 行为。
		 *---------- 
		 */
		while (PageGetFreeSpace(fc_page) < fc_insertstate->itemsz)
		{
			/*
			 * 在考虑向右移动之前，看看我们是否可以通过擦除 LP_DEAD 项目来获得足够的空间
			 */
			if (P_HAS_GARBAGE(fc_opaque))
			{
				/* 执行简单删除 */
				fc__bt_delete_or_dedup_one_page(fc_rel, fc_heapRel, fc_insertstate, true,
											 false, false, false);

				if (PageGetFreeSpace(fc_page) >= fc_insertstate->itemsz)
					break;		/* 好吧，现在我们有足够的空间 */
			}

			/*
			 * 不好，所以检查上述列举的条件 (b) 和 (c)
			 *
			 * 之前的 _bt_check_unique() 调用可能已经为新项目建立了严格的上限偏移量。
			 * 如果它不是页面的最后一个项目（即，如果页面上至少有一个元组大于我们插入的元组）
			 * 那么我们知道这个元组属于这一页。我们可以跳过高关键字检查。
			 */
			if (fc_insertstate->bounds_valid &&
				fc_insertstate->low <= fc_insertstate->stricthigh &&
				fc_insertstate->stricthigh <= PageGetMaxOffsetNumber(fc_page))
				break;

			if (P_RIGHTMOST(fc_opaque) ||
				_bt_compare(fc_rel, fc_itup_key, fc_page, P_HIKEY) != 0 ||
				pg_prng_uint32(&pg_global_prng_state) <= (PG_UINT32_MAX / 100))
				break;

			fc__bt_stepright(fc_rel, fc_insertstate, fc_stack);
			/* 在向右移动后更新本地状态 */
			fc_page = BufferGetPage(fc_insertstate->buf);
			fc_opaque = BTPageGetOpaque(fc_page);
		}
	}

	/*
	 * 我们现在应该在正确的页面上。 找到新元组在页面内的偏移量。
	 * （可能重用早期的搜索范围。）
	 */
	Assert(P_RIGHTMOST(fc_opaque) ||
		   _bt_compare(fc_rel, fc_itup_key, fc_page, P_HIKEY) <= 0);

	fc_newitemoff = _bt_binsrch_insert(fc_rel, fc_insertstate);

	if (fc_insertstate->postingoff == -1)
	{
		/*
		 * 存在一个重叠的发布列表元组，且其 LP_DEAD 位已设置。 
		 * 在执行发布列表拆分时，我们不想不必要地清除其 LP_DEAD 位，
		 * 所以提前执行简单的索引元组删除。
		 */
		fc__bt_delete_or_dedup_one_page(fc_rel, fc_heapRel, fc_insertstate, true,
									 false, false, false);

		/*
		 * 进行新的二分搜索。 新的插入位置现在不能与任何
		 * 发布列表重叠。
		 */
		Assert(!fc_insertstate->bounds_valid);
		fc_insertstate->postingoff = 0;
		fc_newitemoff = _bt_binsrch_insert(fc_rel, fc_insertstate);
		Assert(fc_insertstate->postingoff == 0);
	}

	return fc_newitemoff;
}

/*
 * 在插入过程中向右移动到下一个非死亡页面。
 *
 * 这比在搜索中向右移动要复杂一些。我们必须在释放当前页面的写锁之前
 * 对目标页面进行写锁；否则，其他人的 _bt_check_unique 扫描可能无法
 * 看到我们的插入。在中间的死亡页面上加写锁是不行的，因为我们不知道
 * 它们何时会从树中解除链接。
 *
 * 这对非唯一的 !heapkeyspace 索引来说比必要的更激进。
 */
static void fc__bt_stepright(Relation fc_rel, BTInsertState fc_insertstate, BTStack fc_stack)
{
	Page		fc_page;
	BTPageOpaque fc_opaque;
	Buffer		fc_rbuf;
	BlockNumber fc_rblkno;

	fc_page = BufferGetPage(fc_insertstate->buf);
	fc_opaque = BTPageGetOpaque(fc_page);

	fc_rbuf = InvalidBuffer;
	fc_rblkno = fc_opaque->btpo_next;
	for (;;)
	{
		fc_rbuf = _bt_relandgetbuf(fc_rel, fc_rbuf, fc_rblkno, BT_WRITE);
		fc_page = BufferGetPage(fc_rbuf);
		fc_opaque = BTPageGetOpaque(fc_page);

		/*
		 * 如果这个页面未完成拆分，现在完成拆分。我们在持有左
		 * 兄弟的锁时这样做，这并不好，因为完成拆分可能是一个
		 * 相当耗时的操作。但这应该发生得很少。
		 */
		if (P_INCOMPLETE_SPLIT(fc_opaque))
		{
			_bt_finish_split(fc_rel, fc_rbuf, fc_stack);
			fc_rbuf = InvalidBuffer;
			continue;
		}

		if (!P_IGNORE(fc_opaque))
			break;
		if (P_RIGHTMOST(fc_opaque))
			elog(ERROR, "fell off the end of index \"%s\"",
				 RelationGetRelationName(fc_rel));

		fc_rblkno = fc_opaque->btpo_next;
	}
	/* rbuf 被锁定；解锁缓冲区，更新调用者的状态 */
	_bt_relbuf(fc_rel, fc_insertstate->buf);
	fc_insertstate->buf = fc_rbuf;
	fc_insertstate->bounds_valid = false;
}

/*----------
 *	_bt_insertonpg() -- 在索引中的特定页面上插入一个元组。
 *
 *		这个递归过程执行以下操作：
 *
 *			+  如果 postingoff != 0，拆分现有发布列表元组
 *			   （因为它与新 'itup' 元组重叠）。
 *			+  如果有必要，使用 'itup_key' 进行目标页面的拆分，
 *			   用于叶子页面的后缀截断（调用者在非叶子页面 
 *			   传递 NULL）。
 *			+  插入新的元组（可能是从发布列表拆分得来）。
 *			+  如果页面被拆分，弹出父级堆栈，并找到插入
 *			   新子指针的正确位置（通过使用保存在父级堆栈
 *			   中的信息向右移动）。
 *			+  使用适当的元组调用自身以插入父级的右
 *			   子页面。
 *			+  如果真正的根或快速根被拆分，则更新元数据页面。
 *
 *		在进入时，我们必须有正确的缓冲区以进行插入，
 *		并且该缓冲区必须被固定和写锁定。返回时，
 *		我们将释放缓冲区上的固定和锁。
 *
 *		此例程仅执行零售元组插入。 'itup' 应始终是
 *		非高键叶子项或下链（新高键项是在页面拆分时间接创建的）。
 *		在插入到非叶子页面时，'cbuf' 为我们要插入下链的
 *		页面的左兄弟。此函数将清除它的
 *		INCOMPLETE_SPLIT 标志，并释放缓冲区。
 *----------
 */
static void fc__bt_insertonpg(Relation fc_rel,
			   BTScanInsert fc_itup_key,
			   Buffer fc_buf,
			   Buffer fc_cbuf,
			   BTStack fc_stack,
			   IndexTuple fc_itup,
			   Size fc_itemsz,
			   OffsetNumber fc_newitemoff,
			   int fc_postingoff,
			   bool fc_split_only_page)
{
	Page		fc_page;
	BTPageOpaque fc_opaque;
	bool		fc_isleaf,
				fc_isroot,
				fc_isrightmost,
				fc_isonly;
	IndexTuple	fc_oposting = NULL;
	IndexTuple	fc_origitup = NULL;
	IndexTuple	fc_nposting = NULL;

	fc_page = BufferGetPage(fc_buf);
	fc_opaque = BTPageGetOpaque(fc_page);
	fc_isleaf = P_ISLEAF(fc_opaque);
	fc_isroot = P_ISROOT(fc_opaque);
	fc_isrightmost = P_RIGHTMOST(fc_opaque);
	fc_isonly = P_LEFTMOST(fc_opaque) && P_RIGHTMOST(fc_opaque);

	/* 仅在内部页面插入时必须提供子缓冲区 */
	Assert(fc_isleaf == !BufferIsValid(fc_cbuf));
	/* 元组必须具有适当数量的属性 */
	Assert(!fc_isleaf ||
		   BTreeTupleGetNAtts(fc_itup, fc_rel) ==
		   IndexRelationGetNumberOfAttributes(fc_rel));
	Assert(fc_isleaf ||
		   BTreeTupleGetNAtts(fc_itup, fc_rel) <=
		   IndexRelationGetNumberOfKeyAttributes(fc_rel));
	Assert(!BTreeTupleIsPosting(fc_itup));
	Assert(MAXALIGN(IndexTupleSize(fc_itup)) == fc_itemsz);
	/* 调用者必须始终完成未完成的拆分 */
	Assert(!P_INCOMPLETE_SPLIT(fc_opaque));

	/*
	 * 每个内部页面始终应该有一个负无穷项。只有 _bt_split() 和 _bt_newroot() 
	 * 应该通过截断添加变为负无穷项的项，因为它们是分配新内部页面的唯一例程。
	 */
	Assert(fc_isleaf || fc_newitemoff > P_FIRSTDATAKEY(fc_opaque));

	/*
	 * 我们需要拆分现有的发布列表项吗？
	 */
	if (fc_postingoff != 0)
	{
		ItemId		fc_itemid = PageGetItemId(fc_page, fc_newitemoff);

		/*
		 * 新元组与一个位于叶页面上现有发布列表元组范围内的堆 TID 是重复的。 
		 * 准备拆分现有的发布列表。用其拆分后版本覆盖发布列表被视为 
		 * 插入或页面拆分临界区中的一个额外步骤。
		 */
		Assert(fc_isleaf && fc_itup_key->heapkeyspace && fc_itup_key->allequalimage);
		fc_oposting = (IndexTuple) PageGetItem(fc_page, fc_itemid);

		/*
		 * postingoff 值来源于对 _bt_binsrch_posting() 的早期调用。 
		 * 其二分搜索可能认为一个普通元组必须是需要拆分的发布列表元组。 
		 * 这可能发生在涉及一个现有普通元组的损坏的情况下，该元组是新项的副本， 
		 * 直到包括其表 TID。在此处进行检查。
		 *
		 * 还要验证我们的调用者已经确保现有的发布列表元组没有设置 LP_DEAD 位。
		 */
		if (!BTreeTupleIsPosting(fc_oposting) || ItemIdIsDead(fc_itemid))
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg_internal("table tid from new index tuple (%u,%u) overlaps with invalid duplicate tuple at offset %u of block %u in index \"%s\"",
									 ItemPointerGetBlockNumber(&fc_itup->t_tid),
									 ItemPointerGetOffsetNumber(&fc_itup->t_tid),
									 fc_newitemoff, BufferGetBlockNumber(fc_buf),
									 RelationGetRelationName(fc_rel))));

		/* 从这里开始使用它的可变副本作为我们的 itup */
		fc_origitup = fc_itup;
		fc_itup = CopyIndexTuple(fc_origitup);
		fc_nposting = _bt_swap_posting(fc_itup, fc_oposting, fc_postingoff);
		/* itup 现在包含来自 oposting 的最右侧/最大 TID */

		/* 修改偏移量，以便 newitem 位于发布列表之后 */
		fc_newitemoff = OffsetNumberNext(fc_newitemoff);
	}

	/*
	 * 我们需要拆分页面以适应该项吗？
	 *
	 * 注意：PageGetFreeSpace() 从其结果中减去 sizeof(ItemIdData)，
	 * 因此这个比较是正确的，即使我们似乎只计算项而没有计算其行指针。
	 */
	if (PageGetFreeSpace(fc_page) < fc_itemsz)
	{
		Buffer		fc_rbuf;

		Assert(!fc_split_only_page);

		/* 将缓冲区拆分为左半部分和右半部分 */
		fc_rbuf = fc__bt_split(fc_rel, fc_itup_key, fc_buf, fc_cbuf, fc_newitemoff, fc_itemsz, fc_itup,
						 fc_origitup, fc_nposting, fc_postingoff);
		PredicateLockPageSplit(fc_rel,
							   BufferGetBlockNumber(fc_buf),
							   BufferGetBlockNumber(fc_rbuf));

		/*----------
		 * 到这里，
		 *
		 *		+  我们的目标页面已被拆分；
		 *		+  原始元组已被插入；
		 *		+  在拆分后，我们在旧（左半部分）和新（右半部分）缓冲区上拥有写锁；
		 *		+  我们知道要插入到父节点的键（它是左子页面上的“高键”）。
		 *
		 * 我们准备进行父节点插入。我们需要保持对子页面的锁，直到找到父节点，但
		 * 我们至少可以在进行实际插入之前释放对右子节点的锁。对左子节点的锁
		 * 将在父节点插入时最后释放，它是父页面的 'cbuf'。
		 *----------
		 */
		fc__bt_insert_parent(fc_rel, fc_buf, fc_rbuf, fc_stack, fc_isroot, fc_isonly);
	}
	else
	{
		Buffer		fc_metabuf = InvalidBuffer;
		Page		fc_metapg = NULL;
		BTMetaPageData *fc_metad = NULL;
		BlockNumber fc_blockcache;

		/*
		 * 如果我们这样插入是因为我们拆分了一个在其树级别上的唯一页面，但
		 * 不是根，它可能是“快速根”。我们需要确保快速根链接指向
		 * 当前页面或以上。我们可以安全地在这里获得对元页面的锁 --- 见
		 * _bt_newroot() 的注释。
		 */
		if (unlikely(fc_split_only_page))
		{
			Assert(!fc_isleaf);
			Assert(BufferIsValid(fc_cbuf));

			fc_metabuf = _bt_getbuf(fc_rel, BTREE_METAPAGE, BT_WRITE);
			fc_metapg = BufferGetPage(fc_metabuf);
			fc_metad = BTPageGetMeta(fc_metapg);

			if (fc_metad->btm_fastlevel >= fc_opaque->btpo_level)
			{
				/* 不需要更新 */
				_bt_relbuf(fc_rel, fc_metabuf);
				fc_metabuf = InvalidBuffer;
			}
		}

		/* 执行更新。在更改被记录之前不进行 ereport(ERROR) */
		START_CRIT_SECTION();

		if (fc_postingoff != 0)
			memcpy(fc_oposting, fc_nposting, MAXALIGN(IndexTupleSize(fc_nposting)));

		if (PageAddItem(fc_page, (Item) fc_itup, fc_itemsz, fc_newitemoff, false,
						false) == InvalidOffsetNumber)
			elog(PANIC, "failed to add new item to block %u in index \"%s\"",
				 BufferGetBlockNumber(fc_buf), RelationGetRelationName(fc_rel));

		MarkBufferDirty(fc_buf);

		if (BufferIsValid(fc_metabuf))
		{
			/* 如果需要，升级元页面 */
			if (fc_metad->btm_version < BTREE_NOVAC_VERSION)
				_bt_upgrademetapage(fc_metapg);
			fc_metad->btm_fastroot = BufferGetBlockNumber(fc_buf);
			fc_metad->btm_fastlevel = fc_opaque->btpo_level;
			MarkBufferDirty(fc_metabuf);
		}

		/*
		 * 如果插入新项完成了拆分，则清除子项的 INCOMPLETE_SPLIT 标志
		 */
		if (!fc_isleaf)
		{
			Page		fc_cpage = BufferGetPage(fc_cbuf);
			BTPageOpaque fc_cpageop = BTPageGetOpaque(fc_cpage);

			Assert(P_INCOMPLETE_SPLIT(fc_cpageop));
			fc_cpageop->btpo_flags &= ~BTP_INCOMPLETE_SPLIT;
			MarkBufferDirty(fc_cbuf);
		}

		/* XLOG 相关内容 */
		if (RelationNeedsWAL(fc_rel))
		{
			xl_btree_insert fc_xlrec;
			xl_btree_metadata fc_xlmeta;
			uint8		fc_xlinfo;
			XLogRecPtr	fc_recptr;
			uint16		fc_upostingoff;

			fc_xlrec.offnum = fc_newitemoff;

			XLogBeginInsert();
			XLogRegisterData((char *) &fc_xlrec, SizeOfBtreeInsert);

			if (fc_isleaf && fc_postingoff == 0)
			{
				/* 简单的叶子插入 */
				fc_xlinfo = XLOG_BTREE_INSERT_LEAF;
			}
			else if (fc_postingoff != 0)
			{
				/*
				 * 带有发布列表拆分的叶子插入。必须在 newitem/orignewitem 之前包含 postingoff 字段。
				 */
				Assert(fc_isleaf);
				fc_xlinfo = XLOG_BTREE_INSERT_POST;
			}
			else
			{
				/* 内部页面插入，完成 cbuf 上的拆分 */
				fc_xlinfo = XLOG_BTREE_INSERT_UPPER;
				XLogRegisterBuffer(1, fc_cbuf, REGBUF_STANDARD);

				if (BufferIsValid(fc_metabuf))
				{
					/* 实际上，这是一个内部页面插入 + 元数据更新 */
					fc_xlinfo = XLOG_BTREE_INSERT_META;

					Assert(fc_metad->btm_version >= BTREE_NOVAC_VERSION);
					fc_xlmeta.version = fc_metad->btm_version;
					fc_xlmeta.root = fc_metad->btm_root;
					fc_xlmeta.level = fc_metad->btm_level;
					fc_xlmeta.fastroot = fc_metad->btm_fastroot;
					fc_xlmeta.fastlevel = fc_metad->btm_fastlevel;
					fc_xlmeta.last_cleanup_num_delpages = fc_metad->btm_last_cleanup_num_delpages;
					fc_xlmeta.allequalimage = fc_metad->btm_allequalimage;

					XLogRegisterBuffer(2, fc_metabuf,
									   REGBUF_WILL_INIT | REGBUF_STANDARD);
					XLogRegisterBufData(2, (char *) &fc_xlmeta,
										sizeof(xl_btree_metadata));
				}
			}

			XLogRegisterBuffer(0, fc_buf, REGBUF_STANDARD);
			if (fc_postingoff == 0)
			{
				/* 只需从调用者记录 itup */
				XLogRegisterBufData(0, (char *) fc_itup, IndexTupleSize(fc_itup));
			}
			else
			{
				/*
				 * 带有发布列表拆分的插入（XLOG_BTREE_INSERT_POST 记录）情况。
				 *
				 * 记录 postingoff。还要记录或原始 itup，而不是 itup。REDO 例程
				 * 必须使用 _bt_swap_posting() 重新构建最终 itup（以及 nposting）。
				 */
				fc_upostingoff = fc_postingoff;

				XLogRegisterBufData(0, (char *) &fc_upostingoff, sizeof(uint16));
				XLogRegisterBufData(0, (char *) fc_origitup,
									IndexTupleSize(fc_origitup));
			}

			fc_recptr = XLogInsert(RM_BTREE_ID, fc_xlinfo);

			if (BufferIsValid(fc_metabuf))
				PageSetLSN(fc_metapg, fc_recptr);
			if (!fc_isleaf)
				PageSetLSN(BufferGetPage(fc_cbuf), fc_recptr);

			PageSetLSN(fc_page, fc_recptr);
		}

		END_CRIT_SECTION();

		/* 释放附属缓冲区 */
		if (BufferIsValid(fc_metabuf))
			_bt_relbuf(fc_rel, fc_metabuf);
		if (!fc_isleaf)
			_bt_relbuf(fc_rel, fc_cbuf);

		/*
		 * 如果这是最右侧的叶子页面，则缓存块编号。缓存
		 * 可能会被 _bt_search_insert() 中的未来插入者使用。
		 */
		fc_blockcache = InvalidBlockNumber;
		if (fc_isrightmost && fc_isleaf && !fc_isroot)
			fc_blockcache = BufferGetBlockNumber(fc_buf);

		/* 释放插入目标块的缓冲区 */
		_bt_relbuf(fc_rel, fc_buf);

		/*
		 * 如果我们决定在释放其缓冲区锁之前缓存插入目标块，则现在缓存它。不过，请先检查树的高度。
		 * 我们不会对小索引进行优化。在这一点上推迟最终检查，以确保我们不在持有缓冲区锁的情况下调用 _bt_getrootheight。
		 */
		if (BlockNumberIsValid(fc_blockcache) &&
			_bt_getrootheight(fc_rel) >= BTREE_FASTPATH_MIN_LEVEL)
			RelationSetTargetBlock(fc_rel, fc_blockcache);
	}

	/* 保持整洁 */
	if (fc_postingoff != 0)
	{
		/* itup 实际上是调用者原始的修改副本 */
		pfree(fc_nposting);
		pfree(fc_itup);
	}
}

/*
 *	_bt_split() -- 在 btree 中拆分页面。
 *
 *		进入时，buf 是要拆分的页面，并且已固定并写入锁定。
 *		newitemoff 等告诉我们必须插入的新项，以及来自原始页面的数据。
 *
 *		itup_key 用于叶子页面上的后缀截断（内部页面调用者传递 NULL）。在拆分非叶子页面时，'cbuf'
 *		是我们正在为其插入下行的页面的左侧兄弟。此函数将清除其上的 INCOMPLETE_SPLIT 标志，并
 *		释放缓冲区。
 *
 *		当 orignewitem 的插入导致发布列表拆分和页面拆分时，orignewitem、nposting 和 postingoff 是必需的。
 *		这些额外的发布列表拆分详细信息在这里的使用方式与发布列表拆分不与页面拆分重合的更常见情况相同。
 *		我们需要直接处理发布列表拆分，以确保由于插入 orignewitem 而随之而来的所有内容都被视为一个单一的
 *		原子操作（尽管调用者将新支点/下行插入到父页面的操作仍然是一个单独的操作）。有关发布列表拆分设计的详细信息，请参见
 *		nbtree/README。
 *
 *		返回新右侧兄弟 buf，已固定并写入锁定。
 *		buf 的固定和锁保持不变。
 */
static Buffer fc__bt_split(Relation fc_rel, BTScanInsert fc_itup_key, Buffer fc_buf, Buffer fc_cbuf,
		  OffsetNumber fc_newitemoff, Size fc_newitemsz, IndexTuple fc_newitem,
		  IndexTuple fc_orignewitem, IndexTuple fc_nposting, uint16 fc_postingoff)
{
	Buffer		fc_rbuf;
	Page		fc_origpage;
	Page		fc_leftpage,
				fc_rightpage;
	BlockNumber fc_origpagenumber,
				fc_rightpagenumber;
	BTPageOpaque fc_ropaque,
				fc_lopaque,
				fc_oopaque;
	Buffer		fc_sbuf = InvalidBuffer;
	Page		fc_spage = NULL;
	BTPageOpaque fc_sopaque = NULL;
	Size		fc_itemsz;
	ItemId		fc_itemid;
	IndexTuple	fc_firstright,
				fc_lefthighkey;
	OffsetNumber fc_firstrightoff;
	OffsetNumber fc_afterleftoff,
				fc_afterrightoff,
				fc_minusinfoff;
	OffsetNumber fc_origpagepostingoff;
	OffsetNumber fc_maxoff;
	OffsetNumber fc_i;
	bool		fc_newitemonleft,
				fc_isleaf,
				fc_isrightmost;

	/*
	 * origpage 是要被分割的原始页面。 leftpage 是一个临时
	 * 缓冲区，接收左侧兄弟数据，成功后将复制回 origpage。 rightpage 是新的页面，将接收
	 * 右侧兄弟数据。
	 *
	 * leftpage 在选择分割点后分配。 rightpage 的新
	 * 缓冲区直到 leftpage 初始化并具备新高键后才会获取，这是分割页面可能失败的最后点
	 * （排除损坏）。在获取新缓冲区之前失败不会有持久后果，因为 origpage 不会被修改，且
	 * leftpage 只是工作区。
	 */
	fc_origpage = BufferGetPage(fc_buf);
	fc_oopaque = BTPageGetOpaque(fc_origpage);
	fc_isleaf = P_ISLEAF(fc_oopaque);
	fc_isrightmost = P_RIGHTMOST(fc_oopaque);
	fc_maxoff = PageGetMaxOffsetNumber(fc_origpage);
	fc_origpagenumber = BufferGetBlockNumber(fc_buf);

	/*
	 * 选择一个分割 origpage 的点。
	 *
	 * 分割点可以被视为 origpage 上两个现有数据项之间的一个点
	 * （lastleft 和 firstright 元组），前提是你假装不合适的新项已经在 origpage 上。
	 *
	 * 由于 origpage 实际上不包含 newitem，分割点的表示需要处理两个边界案例：
	 * newitem 是 lastleft 的分割，和 newitem 是 firstright 的分割。
	 * newitemonleft 解决了在 newitemoff == firstrightoff 时可能存在的歧义。在所有其他情况下，从上下文中清楚
	 * 每个元组在哪一侧的分割。 newitemonleft 通常（但不总是）是冗余信息。
	 *
	 * firstrightoff 应该是 origpage 的偏移量，但它的值可能是 maxoff+1，
	 * 这表示“超出 origpage 的结束”。这发生在新项在所有现有项之后的罕见情况下
	 * （即 newitemoff 是 maxoff+1），我们最终在留下新项单独在新右侧页面的点上分割
	 * origpage。任何 "!newitemonleft && newitemoff == firstrightoff" 的分割点使
	 * newitem 成为 firstright 元组，因此这个情况不是特殊情况。
	 */
	fc_firstrightoff = _bt_findsplitloc(fc_rel, fc_origpage, fc_newitemoff, fc_newitemsz,
									 fc_newitem, &fc_newitemonleft);

	/* 为 leftpage 分配临时缓冲区 */
	fc_leftpage = PageGetTempPage(fc_origpage);
	_bt_pageinit(fc_leftpage, BufferGetPageSize(fc_buf));
	fc_lopaque = BTPageGetOpaque(fc_leftpage);

	/*
	 * leftpage 在完成时不会是根。还要清除 SPLIT_END
	 * 和 HAS_GARBAGE 标志。
	 */
	fc_lopaque->btpo_flags = fc_oopaque->btpo_flags;
	fc_lopaque->btpo_flags &= ~(BTP_ROOT | BTP_SPLIT_END | BTP_HAS_GARBAGE);
	/* 在 leftpage 中设置标志，指示 rightpage 尚无下链接 */
	fc_lopaque->btpo_flags |= BTP_INCOMPLETE_SPLIT;
	fc_lopaque->btpo_prev = fc_oopaque->btpo_prev;
	/* 在获取 rightpage 缓冲区后处理 btpo_next */
	fc_lopaque->btpo_level = fc_oopaque->btpo_level;
	/* 在获取 rightpage 缓冲区后处理 btpo_cycleid */

	/*
	 * 将原始页面的 LSN 复制到 leftpage 中，后者将成为页面的
	 * 更新版本。我们需要这个，因为 XLogInsert 将检查 LSN 并可能将其转储到页面图像中。
	 */
	PageSetLSN(fc_leftpage, PageGetLSN(fc_origpage));

	/*
	 * 确定现有的与orignewitem重叠的posting list的页面偏移量
	 * 编号，当有必要在传递中执行posting list拆分时。请注意，
	 * newitem已由调用者更改（newitem不再具有orignewitem TID）。
	 *
	 * 此页面偏移量（origpagepostingoff）将用于假装
	 * posting拆分已经发生，即使对origpage的必要修改
	 * 直到达到关键区域才会发生。我们页面拆分点的lastleft和
	 * firstright元组实际上应来自一个假想版本的origpage，
	 * 它具有nposting元组，而不是原始posting list元组。
	 *
	 * 注意：_bt_findsplitloc()理应以相同的方式
	 * 补偿重合的posting list拆分，至少在理论上是这样。
	 * 不过，它对此不予考虑。在实践中，这不会影响其
	 * 拆分点的选择。
	 */
	fc_origpagepostingoff = InvalidOffsetNumber;
	if (fc_postingoff != 0)
	{
		Assert(fc_isleaf);
		Assert(ItemPointerCompare(&fc_orignewitem->t_tid,
								  &fc_newitem->t_tid) < 0);
		Assert(BTreeTupleIsPosting(fc_nposting));
		fc_origpagepostingoff = OffsetNumberPrev(fc_newitemoff);
	}

	/*
	 * 新左页面的高关键字是leaf级别上firstright的一个可能被截断的副本
	 * （在内部页面上是“firstright本身”；见下面的！isleaf注释）。
	 * 这在表面上似乎与Lehman & Yao在leaf级别上使用
	 * lastleft的副本作为新的高关键字拆分的方法相悖。
	 * 但其实并不是。
	 *
	 * 后缀截断将在lastleft和firstright在堆TID之前相等时
	 * 使左页面的高关键字完全等于lastleft（即，打破平局
	 * 的TID值来自lastleft）。对于L&Y的“子树”不变式
	 * 来说，新leaf高关键字实际上不需要是lastleft的副本。
	 * 确保新leaf高关键字严格小于firstright，并且大于或
	 * 等于（不一定等于）lastleft就足够了。换句话说，
	 * 当在leaf页面拆分期间无法进行后缀截断时，我们采用
	 * L&Y生成新左页面高关键字的确切方法。（实际上，
	 * 这略显不准确。我们并没有仅仅使用lastleft的副本。
	 * 使用的是来自firstright的所有关键字，但max堆TID
	 * 来自lastleft的一个元组，以避免引入特殊情况。）
	 */
	if (!fc_newitemonleft && fc_newitemoff == fc_firstrightoff)
	{
		/* incoming tuple becomes firstright */
		fc_itemsz = fc_newitemsz;
		fc_firstright = fc_newitem;
	}
	else
	{
		/* existing item at firstrightoff becomes firstright */
		fc_itemid = PageGetItemId(fc_origpage, fc_firstrightoff);
		fc_itemsz = ItemIdGetLength(fc_itemid);
		fc_firstright = (IndexTuple) PageGetItem(fc_origpage, fc_itemid);
		if (fc_firstrightoff == fc_origpagepostingoff)
			fc_firstright = fc_nposting;
	}

	if (fc_isleaf)
	{
		IndexTuple	fc_lastleft;

		/* 尝试进行leaf页面拆分的后缀截断 */
		if (fc_newitemonleft && fc_newitemoff == fc_firstrightoff)
		{
			/* incoming tuple becomes lastleft */
			fc_lastleft = fc_newitem;
		}
		else
		{
			OffsetNumber fc_lastleftoff;

			/* existing item before firstrightoff becomes lastleft */
			fc_lastleftoff = OffsetNumberPrev(fc_firstrightoff);
			Assert(fc_lastleftoff >= P_FIRSTDATAKEY(fc_oopaque));
			fc_itemid = PageGetItemId(fc_origpage, fc_lastleftoff);
			fc_lastleft = (IndexTuple) PageGetItem(fc_origpage, fc_itemid);
			if (fc_lastleftoff == fc_origpagepostingoff)
				fc_lastleft = fc_nposting;
		}

		fc_lefthighkey = _bt_truncate(fc_rel, fc_lastleft, fc_firstright, fc_itup_key);
		fc_itemsz = IndexTupleSize(fc_lefthighkey);
	}
	else
	{
		/*
		 * 不要在 firstright 的副本上执行后缀截断，以便为内部页面拆分
		 * 创建左页面的高键。必须直接使用 firstright
		 * 作为新的高键。
		 *
		 * 每个不同的分隔符键值最初作为叶子级别的高键出现；所有其他的分隔符
		 * 键/支点元组都是从下一级复制而来的。祖父页面中的分隔符键必须与
		 * 左侧子树的最右侧父页面中的高键相同，而该高键本身必须与同一子树
		 * 的最右侧子页面中的高键相同（这也适用于来自祖父高键的分隔符）。
		 * 必须始终有一条不间断的“缝隙”由相同的分隔符键引导每个级别的
		 * 索引扫描，从祖父开始。这就是为什么这里执行后缀截断是不安全的原因。
		 *
		 * 内部页面拆分将在新右页面插入时将 firstright 截断为“负
		 * 无穷”数据项。这个过程发生在对右页面的新第一个数据项调用
		 * _bt_pgaddtup() 时。不要将这个机制与后缀截断混淆。这只是
		 * 实现页面拆分的一种方便方式，该拆分在 firstright “内部”
		 * 进行。lefthighkey 分隔符键在右页面中不能再次出现（仅
		 * firstright 的下链接进入右页面）。
		 */
		fc_lefthighkey = fc_firstright;
	}

	/*
	 * 将新高键添加到左页面
	 */
	fc_afterleftoff = P_HIKEY;

	Assert(BTreeTupleGetNAtts(fc_lefthighkey, fc_rel) > 0);
	Assert(BTreeTupleGetNAtts(fc_lefthighkey, fc_rel) <=
		   IndexRelationGetNumberOfKeyAttributes(fc_rel));
	Assert(fc_itemsz == MAXALIGN(IndexTupleSize(fc_lefthighkey)));
	if (PageAddItem(fc_leftpage, (Item) fc_lefthighkey, fc_itemsz, fc_afterleftoff, false,
					false) == InvalidOffsetNumber)
		elog(ERROR, "failed to add high key to the left sibling"
			 " while splitting block %u of index \"%s\"",
			 fc_origpagenumber, RelationGetRelationName(fc_rel));
	fc_afterleftoff = OffsetNumberNext(fc_afterleftoff);

	/*
	 * 获取一个新的右页面以进行拆分，现在左页面有一个新的
	 * 高键。从这里开始，未经先将右页面清零，不可以抛出错误。
	 * 这个编码规则确保我们不会混淆未来的 VACUUM 操作，
	 * 否则它们可能会试图重新找到一个残余垃圾页面的下链接，
	 * 当页面正在被删除。
	 *
	 * 在获取新的右页面缓冲区之后刚开始临界区是合理的；
	 * 这将使我们能够避免残余垃圾页面而无需清零右页面。
	 * 我们这样做是因为这样可以避免在 origpage 或其
	 * 现有兄弟页面损坏时发生不必要的 PANIC。
	 */
	fc_rbuf = _bt_getbuf(fc_rel, P_NEW, BT_WRITE);
	fc_rightpage = BufferGetPage(fc_rbuf);
	fc_rightpagenumber = BufferGetBlockNumber(fc_rbuf);
	/* rightpage 是通过 _bt_getbuf 初始化的 */
	fc_ropaque = BTPageGetOpaque(fc_rightpage);

	/*
	 * 完成剩余的左页面特殊区域字段。它们不能在获得并锁定
	 * origpage（左页面）和右页面缓冲区之前设置。
	 *
	 * btpo_cycleid 仅在叶子页面中使用，但我们在所有情况下这里都
	 * 设置它以保持一致。
	 */
	fc_lopaque->btpo_next = fc_rightpagenumber;
	fc_lopaque->btpo_cycleid = _bt_vacuum_cycleid(fc_rel);

	/*
	 * 完成后 rightpage 不会是根。另请清除 SPLIT_END 和 HAS_GARBAGE 标志。
	 */
	fc_ropaque->btpo_flags = fc_oopaque->btpo_flags;
	fc_ropaque->btpo_flags &= ~(BTP_ROOT | BTP_SPLIT_END | BTP_HAS_GARBAGE);
	fc_ropaque->btpo_prev = fc_origpagenumber;
	fc_ropaque->btpo_next = fc_oopaque->btpo_next;
	fc_ropaque->btpo_level = fc_oopaque->btpo_level;
	fc_ropaque->btpo_cycleid = fc_lopaque->btpo_cycleid;

	/*
	 * 在右页面必要时添加新高键。
	 *
	 * 如果我们正在拆分的页面在树中并不是其级别的最右页面，
	 * 则页面上的第一个条目是来自 origpage 的高键。
	 */
	fc_afterrightoff = P_HIKEY;

	if (!fc_isrightmost)
	{
		IndexTuple	fc_righthighkey;

		fc_itemid = PageGetItemId(fc_origpage, P_HIKEY);
		fc_itemsz = ItemIdGetLength(fc_itemid);
		fc_righthighkey = (IndexTuple) PageGetItem(fc_origpage, fc_itemid);
		Assert(BTreeTupleGetNAtts(fc_righthighkey, fc_rel) > 0);
		Assert(BTreeTupleGetNAtts(fc_righthighkey, fc_rel) <=
			   IndexRelationGetNumberOfKeyAttributes(fc_rel));
		if (PageAddItem(fc_rightpage, (Item) fc_righthighkey, fc_itemsz, fc_afterrightoff,
						false, false) == InvalidOffsetNumber)
		{
			memset(fc_rightpage, 0, BufferGetPageSize(fc_rbuf));
			elog(ERROR, "failed to add high key to the right sibling"
				 " while splitting block %u of index \"%s\"",
				 fc_origpagenumber, RelationGetRelationName(fc_rel));
		}
		fc_afterrightoff = OffsetNumberNext(fc_afterrightoff);
	}

	/*
	 * 内部页面拆分将在右页面截断第一个数据项——它成为该页的
	 * “负无穷”项。在这里设置。
	 */
	fc_minusinfoff = InvalidOffsetNumber;
	if (!fc_isleaf)
		fc_minusinfoff = fc_afterrightoff;

	/*
	 * 现在将所有数据项（在 isleaf 情况下的非透视元组，或在 !isleaf 情况下的额外透视元组）转移到适当的页面。
	 *
	 * 注意：我们 *必须* 按 item-number 顺序插入右侧页面的项目，以便于 _bt_restore_page() 的使用。
	 */
	for (fc_i = P_FIRSTDATAKEY(fc_oopaque); fc_i <= fc_maxoff; fc_i = OffsetNumberNext(fc_i))
	{
		IndexTuple	fc_dataitem;

		fc_itemid = PageGetItemId(fc_origpage, fc_i);
		fc_itemsz = ItemIdGetLength(fc_itemid);
		fc_dataitem = (IndexTuple) PageGetItem(fc_origpage, fc_itemid);

		/* 由于发布分拆，是否用 nposting 替换原始项？ */
		if (fc_i == fc_origpagepostingoff)
		{
			Assert(BTreeTupleIsPosting(fc_dataitem));
			Assert(fc_itemsz == MAXALIGN(IndexTupleSize(fc_nposting)));
			fc_dataitem = fc_nposting;
		}

		/* 新项是否应在此项之前？ */
		else if (fc_i == fc_newitemoff)
		{
			if (fc_newitemonleft)
			{
				Assert(fc_newitemoff <= fc_firstrightoff);
				if (!fc__bt_pgaddtup(fc_leftpage, fc_newitemsz, fc_newitem, fc_afterleftoff,
								  false))
				{
					memset(fc_rightpage, 0, BufferGetPageSize(fc_rbuf));
					elog(ERROR, "failed to add new item to the left sibling"
						 " while splitting block %u of index \"%s\"",
						 fc_origpagenumber, RelationGetRelationName(fc_rel));
				}
				fc_afterleftoff = OffsetNumberNext(fc_afterleftoff);
			}
			else
			{
				Assert(fc_newitemoff >= fc_firstrightoff);
				if (!fc__bt_pgaddtup(fc_rightpage, fc_newitemsz, fc_newitem, fc_afterrightoff,
								  fc_afterrightoff == fc_minusinfoff))
				{
					memset(fc_rightpage, 0, BufferGetPageSize(fc_rbuf));
					elog(ERROR, "failed to add new item to the right sibling"
						 " while splitting block %u of index \"%s\"",
						 fc_origpagenumber, RelationGetRelationName(fc_rel));
				}
				fc_afterrightoff = OffsetNumberNext(fc_afterrightoff);
			}
		}

		/* 决定将其放置在哪个页面上 */
		if (fc_i < fc_firstrightoff)
		{
			if (!fc__bt_pgaddtup(fc_leftpage, fc_itemsz, fc_dataitem, fc_afterleftoff, false))
			{
				memset(fc_rightpage, 0, BufferGetPageSize(fc_rbuf));
				elog(ERROR, "failed to add old item to the left sibling"
					 " while splitting block %u of index \"%s\"",
					 fc_origpagenumber, RelationGetRelationName(fc_rel));
			}
			fc_afterleftoff = OffsetNumberNext(fc_afterleftoff);
		}
		else
		{
			if (!fc__bt_pgaddtup(fc_rightpage, fc_itemsz, fc_dataitem, fc_afterrightoff,
							  fc_afterrightoff == fc_minusinfoff))
			{
				memset(fc_rightpage, 0, BufferGetPageSize(fc_rbuf));
				elog(ERROR, "failed to add old item to the right sibling"
					 " while splitting block %u of index \"%s\"",
					 fc_origpagenumber, RelationGetRelationName(fc_rel));
			}
			fc_afterrightoff = OffsetNumberNext(fc_afterrightoff);
		}
	}

	/* 处理新项放在右页面末尾的情况 */
	if (fc_i <= fc_newitemoff)
	{
		/*
		 * 这里不能有 newitemonleft；那将意味着我们被告知将 *所有* 内容放在左侧页面，但左侧页面无法容纳（如果可以，我们就不会拆分页面）。
		 */
		Assert(!fc_newitemonleft && fc_newitemoff == fc_maxoff + 1);
		if (!fc__bt_pgaddtup(fc_rightpage, fc_newitemsz, fc_newitem, fc_afterrightoff,
						  fc_afterrightoff == fc_minusinfoff))
		{
			memset(fc_rightpage, 0, BufferGetPageSize(fc_rbuf));
			elog(ERROR, "failed to add new item to the right sibling"
				 " while splitting block %u of index \"%s\"",
				 fc_origpagenumber, RelationGetRelationName(fc_rel));
		}
		fc_afterrightoff = OffsetNumberNext(fc_afterrightoff);
	}

	/*
	 * 我们必须获取原始的右兄弟（如果有）并更新其 prev 链接。我们可以保证这是无死锁的，因为我们以标准顺序锁定：从左到右。
	 */
	if (!fc_isrightmost)
	{
		fc_sbuf = _bt_getbuf(fc_rel, fc_oopaque->btpo_next, BT_WRITE);
		fc_spage = BufferGetPage(fc_sbuf);
		fc_sopaque = BTPageGetOpaque(fc_spage);
		if (fc_sopaque->btpo_prev != fc_origpagenumber)
		{
			memset(fc_rightpage, 0, BufferGetPageSize(fc_rbuf));
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg_internal("right sibling's left-link doesn't match: "
									 "block %u links to %u instead of expected %u in index \"%s\"",
									 fc_oopaque->btpo_next, fc_sopaque->btpo_prev, fc_origpagenumber,
									 RelationGetRelationName(fc_rel))));
		}

		/*
		 * 检查我们是否可以在右侧拆分页面设置 SPLIT_END 标志；这可以为清理节省一些 I/O，因为它不必继续到右兄弟。我们可以设置该标志，如果右兄弟具有不同的 cycleid：这意味着它不可能是从同一祖先页面拆分出的页面组的一部分。如果你感到困惑，想象页面 A 拆分为 A B，然后再次拆分，产生 A C B，而清理正在进行中。最初在 A 中的元组现在可以在 B 或 C 中，因此清理必须检查这两个页面。但如果 D，我们的右兄弟，具有不同的 cycleid，那么它不可能包含在清理开始时在 A 中的任何元组。
		 */
		if (fc_sopaque->btpo_cycleid != fc_ropaque->btpo_cycleid)
			fc_ropaque->btpo_flags |= BTP_SPLIT_END;
	}

	/*
	 * 右兄弟已被锁定，新兄弟已准备好，但原始页面尚未更新。
	 *
	 * 在右兄弟更新之前没有 EREPORT(ERROR)。我们可以在这里暂时不开始关键段，因为我们尚未在原始页面上涂改；见上面的评论。
	 */
	START_CRIT_SECTION();

	/*
	 * 到这里为止，原始数据页面已拆分为两个新部分，这些部分是正确的。算法要求在拆分期间左侧页面不能移动，因此我们将新的左侧页面复制回原始页面上方。我们需要在写入 WAL 记录之前执行此操作，以便在必要时 XLogInsert 可以将页面的图像记录到 WAL。
	 */
	PageRestoreTempPage(fc_leftpage, fc_origpage);
	/* leftpage, lopaque 在这里以下不能使用 */

	MarkBufferDirty(fc_buf);
	MarkBufferDirty(fc_rbuf);

	if (!fc_isrightmost)
	{
		fc_sopaque->btpo_prev = fc_rightpagenumber;
		MarkBufferDirty(fc_sbuf);
	}

	/*
	 * 如果插入新项完成拆分，则清除子项上的 INCOMPLETE_SPLIT 标志
	 */
	if (!fc_isleaf)
	{
		Page		fc_cpage = BufferGetPage(fc_cbuf);
		BTPageOpaque fc_cpageop = BTPageGetOpaque(fc_cpage);

		fc_cpageop->btpo_flags &= ~BTP_INCOMPLETE_SPLIT;
		MarkBufferDirty(fc_cbuf);
	}

	/* XLOG 相关内容 */
	if (RelationNeedsWAL(fc_rel))
	{
		xl_btree_split fc_xlrec;
		uint8		fc_xlinfo;
		XLogRecPtr	fc_recptr;

		fc_xlrec.level = fc_ropaque->btpo_level;
		/* 请参见下面关于 newitem、orignewitem 和发布列表的评论 */
		fc_xlrec.firstrightoff = fc_firstrightoff;
		fc_xlrec.newitemoff = fc_newitemoff;
		fc_xlrec.postingoff = 0;
		if (fc_postingoff != 0 && fc_origpagepostingoff < fc_firstrightoff)
			fc_xlrec.postingoff = fc_postingoff;

		XLogBeginInsert();
		XLogRegisterData((char *) &fc_xlrec, SizeOfBtreeSplit);

		XLogRegisterBuffer(0, fc_buf, REGBUF_STANDARD);
		XLogRegisterBuffer(1, fc_rbuf, REGBUF_WILL_INIT);
		/* 记录原始右兄弟，因为我们已更改其 prev-pointer */
		if (!fc_isrightmost)
			XLogRegisterBuffer(2, fc_sbuf, REGBUF_STANDARD);
		if (!fc_isleaf)
			XLogRegisterBuffer(3, fc_cbuf, REGBUF_STANDARD);

		/*
		 * 记录新项，如果它被插入到左侧页面。（如果它被放在右侧页面，我们不需要显式地进行WAL日志记录，因为它包含在右侧页面的所有其他项中。）
		 * 将新项显示为属于左侧页面缓冲区，以便如果XLogInsert决定需要左侧页面的完整页面图像，就不存储它。不过我们总是将newitemoff存储在记录中。
		 *
		 * 对于与发布列表拆分重合的页面拆分，细节有时会略有不同。如果替换的发布列表和新项都放在右侧页面，那么我们就不需要记录额外的内容，就像简单的!newitemonleft没有发布拆分的情况（postingoff在WAL记录中设置为零，因此恢复根本不需要处理发布列表拆分）。否则，我们设置postingoff并记录orignewitem而不是newitem，尽管实际上已经插入了newitem。REDO例程必须使用_bt_swap_posting()重建nposting和newitem。
		 *
		 * 注意：我们的页面拆分点可能是使得发布列表lastleft和新项firstright的点。这是唯一一个即使新项放在右侧页面，我们仍记录orignewitem/newitem的情况。如果XLogInsert决定由于记录左侧页面的完整页面图像可以省略orignewitem，一切仍然能够正常工作，因为恢复只需要为左侧页面上的项记录orignewitem（就像常规的newitem-logged情况）。
		 */
		if (fc_newitemonleft && fc_xlrec.postingoff == 0)
			XLogRegisterBufData(0, (char *) fc_newitem, fc_newitemsz);
		else if (fc_xlrec.postingoff != 0)
		{
			Assert(fc_isleaf);
			Assert(fc_newitemonleft || fc_firstrightoff == fc_newitemoff);
			Assert(fc_newitemsz == IndexTupleSize(fc_orignewitem));
			XLogRegisterBufData(0, (char *) fc_orignewitem, fc_newitemsz);
		}

		/* 记录左侧页面的新高键 */
		if (!fc_isleaf)
		{
			/* lefthighkey不是本地副本，获得当前指针 */
			fc_itemid = PageGetItemId(fc_origpage, P_HIKEY);
			fc_lefthighkey = (IndexTuple) PageGetItem(fc_origpage, fc_itemid);
		}
		XLogRegisterBufData(0, (char *) fc_lefthighkey,
							MAXALIGN(IndexTupleSize(fc_lefthighkey)));

		/*
		 * 以_bt_restore_page()理解的格式记录右侧页面的内容。整个右侧页面将被重新创建。
		 *
		 * 直接访问页面并不好，但更快 - 我们应该在页面API中实现一些新功能。注意我们只存储元组本身，知道它们是按项号顺序插入的，因此可以重建行指针。有关_bt_restore_page()的注释，请参见。
		 */
		XLogRegisterBufData(1,
							(char *) fc_rightpage + ((PageHeader) fc_rightpage)->pd_upper,
							((PageHeader) fc_rightpage)->pd_special - ((PageHeader) fc_rightpage)->pd_upper);

		fc_xlinfo = fc_newitemonleft ? XLOG_BTREE_SPLIT_L : XLOG_BTREE_SPLIT_R;
		fc_recptr = XLogInsert(RM_BTREE_ID, fc_xlinfo);

		PageSetLSN(fc_origpage, fc_recptr);
		PageSetLSN(fc_rightpage, fc_recptr);
		if (!fc_isrightmost)
			PageSetLSN(fc_spage, fc_recptr);
		if (!fc_isleaf)
			PageSetLSN(BufferGetPage(fc_cbuf), fc_recptr);
	}

	END_CRIT_SECTION();

	/* 释放旧的右侧兄弟 */
	if (!fc_isrightmost)
		_bt_relbuf(fc_rel, fc_sbuf);

	/* 释放子项 */
	if (!fc_isleaf)
		_bt_relbuf(fc_rel, fc_cbuf);

	/* 保持整洁 */
	if (fc_isleaf)
		pfree(fc_lefthighkey);

	/* 拆分完成 */
	return fc_rbuf;
}

/*
 * _bt_insert_parent() -- 将下行链接插入父节点，完成分裂。
 *
 * 进入时，buf 和 rbuf 是左右分裂页面，我们仍然持有写锁。两个锁将在这里释放。我们在对我们打算插入下行链接到 rbuf 的页面（即 buf 当前的父页面）获得写锁后释放 rbuf 的锁。buf 的锁与父页面的锁在同一点释放，因为 buf 的 INCOMPLETE_SPLIT 标志必须由完成插入新下行链接的同一个原子操作清除。
 *
 * stack - 显示我们如何到达这里的堆栈。当分裂真正的根节点时，或者在并发根节点分裂期间，堆栈将为 NULL，这时我们可能效率不高。
 * isroot - 我们分裂了真正的根节点。
 * isonly - 我们在其级别上单独分裂了一个页面（可能是快速根节点）。
 */
static void fc__bt_insert_parent(Relation fc_rel,
				  Buffer fc_buf,
				  Buffer fc_rbuf,
				  BTStack fc_stack,
				  bool fc_isroot,
				  bool fc_isonly)
{
	/*
	 * 在这里我们必须做一些 Lehman 和 Yao 没有谈到的事情：处理根分裂和构造新根。 如果我们的堆栈为空，则我们刚刚在我们下降树时曾为根级别的节点上进行了分裂。 如果它仍然是根，则我们执行一个新根构造。 如果它*不是*根节点了，搜索以找到某人同时构造的下一个更高的级别，并找到合适的位置进行插入，像正常情况一样。
	 *
	 * 如果我们必须搜索父级，只需从根重新下降即可。 这不是超级高效，但发生的频率足够低，以至于不必担心。
	 */
	if (fc_isroot)
	{
		Buffer		fc_rootbuf;

		Assert(fc_stack == NULL);
		Assert(fc_isonly);
		/* 创建一个新的根节点并更新元页面 */
		fc_rootbuf = fc__bt_newroot(fc_rel, fc_buf, fc_rbuf);
		/* 释放分裂缓冲区 */
		_bt_relbuf(fc_rel, fc_rootbuf);
		_bt_relbuf(fc_rel, fc_rbuf);
		_bt_relbuf(fc_rel, fc_buf);
	}
	else
	{
		BlockNumber fc_bknum = BufferGetBlockNumber(fc_buf);
		BlockNumber fc_rbknum = BufferGetBlockNumber(fc_rbuf);
		Page		fc_page = BufferGetPage(fc_buf);
		IndexTuple	fc_new_item;
		BTStackData fc_fakestack;
		IndexTuple	fc_ritem;
		Buffer		fc_pbuf;

		if (fc_stack == NULL)
		{
			BTPageOpaque fc_opaque;

			elog(DEBUG2, "concurrent ROOT page split");
			fc_opaque = BTPageGetOpaque(fc_page);

			/*
			 * 当发生叶页分裂时，我们绝不会到达这里，尽管新项的插入能够应用快速路径优化。 确保这一点，使用断言。
			 *
			 * 这是一个性能问题，而不是正确性问题。 快速路径将不会有下降堆栈。 在这里使用一个虚假的堆栈是有效的，但永远不要依赖它。 当最右边的叶页面将要分裂时，应该在 _bt_search_insert() 中拒绝快速路径，因为通过 _bt_search() 以通常的方式获取堆栈会更快。
			 */
			Assert(!(P_ISLEAF(fc_opaque) &&
					 BlockNumberIsValid(RelationGetTargetBlock(fc_rel))));

			/* 找到下一个级别最左边的页面 */
			fc_pbuf = _bt_get_endpoint(fc_rel, fc_opaque->btpo_level + 1, false, NULL);
			/* 设置指向该处的虚假堆栈条目 */
			fc_stack = &fc_fakestack;
			fc_stack->bts_blkno = BufferGetBlockNumber(fc_pbuf);
			fc_stack->bts_offset = InvalidOffsetNumber;
			fc_stack->bts_parent = NULL;
			_bt_relbuf(fc_rel, fc_pbuf);
		}

		/* 从左侧获取高键，作为新右页面的严格下界 */
		fc_ritem = (IndexTuple) PageGetItem(fc_page,
										 PageGetItemId(fc_page, P_HIKEY));

		/* 形成一个指向新右页面的索引元组 */
		fc_new_item = CopyIndexTuple(fc_ritem);
		BTreeTupleSetDownLink(fc_new_item, fc_rbknum);

		/*
		 * 重新查找并写锁 buf 的父级。
		 *
		 * buf 的下行链接的位置可能自我们最初的 _bt_search() 下降以来发生了变化。 _bt_getstackbuf() 将检测并恢复此情况，更新堆栈，这确保新的下行链接将插入到正确的偏移量。 即使 buf 的父级也可能已经改变。
		 */
		fc_pbuf = _bt_getstackbuf(fc_rel, fc_stack, fc_bknum);

		/*
		 * 解锁右子节点。左子节点将在
		 * _bt_insertonpg() 中解锁。
		 *
		 * 需要将解锁右子节点的操作延迟到此，以确保没有并发的 VACUUM 操作会造成混淆。页面删除
		 * 不得允许未能重新找到 rbuf 页面的下链接。
		 * （实际上，这只是过去工作方式的遗留。页面删除代码预计将在左子节点上检查
		 * INCOMPLETE_SPLIT 标志。在分裂完成之前，它不会尝试删除右子节点。尽管如此，我们选择
		 * 保守地延迟在此解锁右子节点。）
		 */
		_bt_relbuf(fc_rel, fc_rbuf);

		if (fc_pbuf == InvalidBuffer)
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg_internal("failed to re-find parent key in index \"%s\" for split pages %u/%u",
									 RelationGetRelationName(fc_rel), fc_bknum, fc_rbknum)));

		/* 递归插入到父节点 */
		fc__bt_insertonpg(fc_rel, NULL, fc_pbuf, fc_buf, fc_stack->bts_parent,
					   fc_new_item, MAXALIGN(IndexTupleSize(fc_new_item)),
					   fc_stack->bts_offset + 1, 0, fc_isonly);

		/* 保持整洁 */
		pfree(fc_new_item);
	}
}

/*
 * _bt_finish_split() -- 完成不完整的分裂
 *
 * 崩溃或其他故障可能导致分裂不完整。插入
 * 例程不允许在不完整分裂的页面上插入。
 * 在插入此类页面之前，调用 _bt_finish_split()。
 *
 * 进入时，'lbuf' 必须在写入模式下被锁定。退出时，它会被解锁
 * 并取消固定。
 */
void _bt_finish_split(Relation fc_rel, Buffer fc_lbuf, BTStack fc_stack)
{
	Page		fc_lpage = BufferGetPage(fc_lbuf);
	BTPageOpaque fc_lpageop = BTPageGetOpaque(fc_lpage);
	Buffer		fc_rbuf;
	Page		fc_rpage;
	BTPageOpaque fc_rpageop;
	bool		fc_wasroot;
	bool		fc_wasonly;

	Assert(P_INCOMPLETE_SPLIT(fc_lpageop));

	/* 锁定右兄弟，缺少下链接的那一个 */
	fc_rbuf = _bt_getbuf(fc_rel, fc_lpageop->btpo_next, BT_WRITE);
	fc_rpage = BufferGetPage(fc_rbuf);
	fc_rpageop = BTPageGetOpaque(fc_rpage);

	/* 这会是根分裂吗？ */
	if (!fc_stack)
	{
		Buffer		fc_metabuf;
		Page		fc_metapg;
		BTMetaPageData *fc_metad;

		/* 获取元数据页面的锁 */
		fc_metabuf = _bt_getbuf(fc_rel, BTREE_METAPAGE, BT_WRITE);
		fc_metapg = BufferGetPage(fc_metabuf);
		fc_metad = BTPageGetMeta(fc_metapg);

		fc_wasroot = (fc_metad->btm_root == BufferGetBlockNumber(fc_lbuf));

		_bt_relbuf(fc_rel, fc_metabuf);
	}
	else
		fc_wasroot = false;

	/* 这是分裂前该层唯一的页面吗？ */
	fc_wasonly = (P_LEFTMOST(fc_lpageop) && P_RIGHTMOST(fc_rpageop));

	elog(DEBUG1, "finishing incomplete split of %u/%u",
		 BufferGetBlockNumber(fc_lbuf), BufferGetBlockNumber(fc_rbuf));

	fc__bt_insert_parent(fc_rel, fc_lbuf, fc_rbuf, fc_stack, fc_wasroot, fc_wasonly);
}

/*
 *	_bt_getstackbuf() -- 向上走一步，找到指向子页面的下链接的支点
 *						 元组。
 *
 *		调用者传递子块号，该块号用于在父页面中识别
 *		关联的支点元组，使用与支点的下链接/块号匹配的线性搜索。
 *		支点元组的预期位置取自子页面上方一层的栈。
 *		这作为起始点。在
 *		父层的插入操作可能会导致支点元组向右移动；删除
 *		可能会导致其向左移动，但不能左移到我们之前
 *		找到的页面之外。
 *
 *		调用者可以使用其栈将支点元组/下链接重新定位到
 *		其初始下降时找到的页面右侧的同层页面。这是必要的，因为调用者
 *		可能向右移动以从并发页面分裂中恢复。当没有
 *		并发页面分裂时，对于某些调用者能够向右步进同时仍使用其原始
 *		栈是方便的。例如，checkingunique _bt_doinsert() 案例可能
 *		在物理重复项很多时必须步进右侧，而其
 *		scantid 强制插入到“值可能所在的第一个页面”的右侧。（当处理 !heapkeyspace 索引时，
 *		这也被我们所有的调用者依赖。）
 *
 *		返回写锁定的父页面缓冲区，如果未找到支点
 *		元组则返回 InvalidBuffer（不应发生）。如果改变，则调整 bts_blkno &
 *		bts_offset。页面分裂调用者应在父页面的
 *		偏移量 bts_offset + 1 处插入其新右兄弟页面的新
 *		支点元组。
*/
Buffer _bt_getstackbuf(Relation fc_rel, BTStack fc_stack, BlockNumber fc_child)
{
	BlockNumber fc_blkno;
	OffsetNumber fc_start;

	fc_blkno = fc_stack->bts_blkno;
	fc_start = fc_stack->bts_offset;

	for (;;)
	{
		Buffer		fc_buf;
		Page		fc_page;
		BTPageOpaque fc_opaque;

		fc_buf = _bt_getbuf(fc_rel, fc_blkno, BT_WRITE);
		fc_page = BufferGetPage(fc_buf);
		fc_opaque = BTPageGetOpaque(fc_page);

		if (P_INCOMPLETE_SPLIT(fc_opaque))
		{
			_bt_finish_split(fc_rel, fc_buf, fc_stack->bts_parent);
			continue;
		}

		if (!P_IGNORE(fc_opaque))
		{
			OffsetNumber fc_offnum,
						fc_minoff,
						fc_maxoff;
			ItemId		fc_itemid;
			IndexTuple	fc_item;

			fc_minoff = P_FIRSTDATAKEY(fc_opaque);
			fc_maxoff = PageGetMaxOffsetNumber(fc_page);

			/*
			 * start = InvalidOffsetNumber意味着“搜索整个页面”。我们
			 * 需要这个测试，因为页面现在可能有一个高
			 * 密钥，而之前没有。
			 */
			if (fc_start < fc_minoff)
				fc_start = fc_minoff;

			/*
			 * 这个检查也是必要的，以防我们原始访问时页面
			 * 已经分裂。
			 */
			if (fc_start > fc_maxoff)
				fc_start = OffsetNumberNext(fc_maxoff);

			/*
			 * 这些循环将检查页面上的每个项目——但顺序与
			 * 实际位置的概率相适应。首先向右扫描，然后向左。
			 */
			for (fc_offnum = fc_start;
				 fc_offnum <= fc_maxoff;
				 fc_offnum = OffsetNumberNext(fc_offnum))
			{
				fc_itemid = PageGetItemId(fc_page, fc_offnum);
				fc_item = (IndexTuple) PageGetItem(fc_page, fc_itemid);

				if (BTreeTupleGetDownLink(fc_item) == fc_child)
				{
					/* 返回链接当前位置的准确指针 */
					fc_stack->bts_blkno = fc_blkno;
					fc_stack->bts_offset = fc_offnum;
					return fc_buf;
				}
			}

			for (fc_offnum = OffsetNumberPrev(fc_start);
				 fc_offnum >= fc_minoff;
				 fc_offnum = OffsetNumberPrev(fc_offnum))
			{
				fc_itemid = PageGetItemId(fc_page, fc_offnum);
				fc_item = (IndexTuple) PageGetItem(fc_page, fc_itemid);

				if (BTreeTupleGetDownLink(fc_item) == fc_child)
				{
					/* 返回链接当前位置的准确指针 */
					fc_stack->bts_blkno = fc_blkno;
					fc_stack->bts_offset = fc_offnum;
					return fc_buf;
				}
			}
		}

		/*
		 * 我们正在寻找的项目至少向右移动了一页。
		 *
		 * Lehman和Yao在这里向右移动时使用了成对/链式锁，
		 * 但我们可以避免。请参阅nbtree/README。
		 */
		if (P_RIGHTMOST(fc_opaque))
		{
			_bt_relbuf(fc_rel, fc_buf);
			return InvalidBuffer;
		}
		fc_blkno = fc_opaque->btpo_next;
		fc_start = InvalidOffsetNumber;
		_bt_relbuf(fc_rel, fc_buf);
	}
}

/*
 *	_bt_newroot() -- 为索引创建一个新的根页面。
 *
 *		我们刚刚分裂了旧的根页面，需要创建一个新的。
 *		为了做到这一点，我们向文件添加一个新的根页面，
 *		然后锁定元数据页面并更新它。这保证是无死锁的，
 *		因为所有读取者在尝试锁定根之前释放其对元数据页面的锁，
 *		而所有写入者在尝试锁定元数据页面之前锁定根。我们在旧的
 *		根页面上有一个写锁，因此我们没有在等待图中引入任何循环。
 *
 *		进入时，lbuf（旧根）和rbuf（其新对等体）是写锁定的。
 *		退出时，存在一个新的根页面，其具有两个新子项的条目，
 *		元数据页面被更新并解锁/取消固定。
 *		新的根缓冲区被返回给调用者，调用者必须解锁/取消固定
 *		lbuf，rbuf和rootbuf。
 */
static Buffer fc__bt_newroot(Relation fc_rel, Buffer fc_lbuf, Buffer fc_rbuf)
{
	Buffer		fc_rootbuf;
	Page		fc_lpage,
				fc_rootpage;
	BlockNumber fc_lbkno,
				fc_rbkno;
	BlockNumber fc_rootblknum;
	BTPageOpaque fc_rootopaque;
	BTPageOpaque fc_lopaque;
	ItemId		fc_itemid;
	IndexTuple	fc_item;
	IndexTuple	fc_left_item;
	Size		fc_left_item_sz;
	IndexTuple	fc_right_item;
	Size		fc_right_item_sz;
	Buffer		fc_metabuf;
	Page		fc_metapg;
	BTMetaPageData *fc_metad;

	fc_lbkno = BufferGetBlockNumber(fc_lbuf);
	fc_rbkno = BufferGetBlockNumber(fc_rbuf);
	fc_lpage = BufferGetPage(fc_lbuf);
	fc_lopaque = BTPageGetOpaque(fc_lpage);

	/* 获取一个新的根页面 */
	fc_rootbuf = _bt_getbuf(fc_rel, P_NEW, BT_WRITE);
	fc_rootpage = BufferGetPage(fc_rootbuf);
	fc_rootblknum = BufferGetBlockNumber(fc_rootbuf);

	/* 获取元数据页面的锁 */
	fc_metabuf = _bt_getbuf(fc_rel, BTREE_METAPAGE, BT_WRITE);
	fc_metapg = BufferGetPage(fc_metabuf);
	fc_metad = BTPageGetMeta(fc_metapg);

	/*
	 * 为左页面（旧根）创建下链接项。使用的键值为
	 * “负无穷大”，这是一个可靠小于左页面中任意实际
	 * 键值的哨兵值。
	 */
	fc_left_item_sz = sizeof(IndexTupleData);
	fc_left_item = (IndexTuple) palloc(fc_left_item_sz);
	fc_left_item->t_info = fc_left_item_sz;
	BTreeTupleSetDownLink(fc_left_item, fc_lbkno);
	BTreeTupleSetNAtts(fc_left_item, 0, false);

	/*
	 * 为右页面创建下链接项。它的键值来自
	 * 左页面中的“高键”位置。
	 */
	fc_itemid = PageGetItemId(fc_lpage, P_HIKEY);
	fc_right_item_sz = ItemIdGetLength(fc_itemid);
	fc_item = (IndexTuple) PageGetItem(fc_lpage, fc_itemid);
	fc_right_item = CopyIndexTuple(fc_item);
	BTreeTupleSetDownLink(fc_right_item, fc_rbkno);

	/* 从这里到新的根操作记录之前没有ERREPORT(ERROR) */
	START_CRIT_SECTION();

	/* 根据需要升级元数据页面 */
	if (fc_metad->btm_version < BTREE_NOVAC_VERSION)
		_bt_upgrademetapage(fc_metapg);

	/* 设置btree特别数据 */
	fc_rootopaque = BTPageGetOpaque(fc_rootpage);
	fc_rootopaque->btpo_prev = fc_rootopaque->btpo_next = P_NONE;
	fc_rootopaque->btpo_flags = BTP_ROOT;
	fc_rootopaque->btpo_level =
		(BTPageGetOpaque(fc_lpage))->btpo_level + 1;
	fc_rootopaque->btpo_cycleid = 0;

	/* 更新元数据页面数据 */
	fc_metad->btm_root = fc_rootblknum;
	fc_metad->btm_level = fc_rootopaque->btpo_level;
	fc_metad->btm_fastroot = fc_rootblknum;
	fc_metad->btm_fastlevel = fc_rootopaque->btpo_level;

	/*
	 * 将左页面指针插入新的根页面。根页面是
	 * 该级别最右边的页面，因此没有“高键”；
	 * 这两个项将进入位置P_HIKEY和P_FIRSTKEY。
	 *
	 * 注意：我们*必须*按项目编号顺序插入这两个项，
	 * 以便于_bt_restore_page()。
	 */
	Assert(BTreeTupleGetNAtts(fc_left_item, fc_rel) == 0);
	if (PageAddItem(fc_rootpage, (Item) fc_left_item, fc_left_item_sz, P_HIKEY,
					false, false) == InvalidOffsetNumber)
		elog(PANIC, "failed to add leftkey to new root page"
			 " while splitting block %u of index \"%s\"",
			 BufferGetBlockNumber(fc_lbuf), RelationGetRelationName(fc_rel));

	/*
	 * 将右页面指针插入新的根页面。
	 */
	Assert(BTreeTupleGetNAtts(fc_right_item, fc_rel) > 0);
	Assert(BTreeTupleGetNAtts(fc_right_item, fc_rel) <=
		   IndexRelationGetNumberOfKeyAttributes(fc_rel));
	if (PageAddItem(fc_rootpage, (Item) fc_right_item, fc_right_item_sz, P_FIRSTKEY,
					false, false) == InvalidOffsetNumber)
		elog(PANIC, "failed to add rightkey to new root page"
			 " while splitting block %u of index \"%s\"",
			 BufferGetBlockNumber(fc_lbuf), RelationGetRelationName(fc_rel));

	/* 清除左子项中的不完整分裂标志 */
	Assert(P_INCOMPLETE_SPLIT(fc_lopaque));
	fc_lopaque->btpo_flags &= ~BTP_INCOMPLETE_SPLIT;
	MarkBufferDirty(fc_lbuf);

	MarkBufferDirty(fc_rootbuf);
	MarkBufferDirty(fc_metabuf);

	/* XLOG 相关内容 */
	if (RelationNeedsWAL(fc_rel))
	{
		xl_btree_newroot fc_xlrec;
		XLogRecPtr	fc_recptr;
		xl_btree_metadata fc_md;

		fc_xlrec.rootblk = fc_rootblknum;
		fc_xlrec.level = fc_metad->btm_level;

		XLogBeginInsert();
		XLogRegisterData((char *) &fc_xlrec, SizeOfBtreeNewroot);

		XLogRegisterBuffer(0, fc_rootbuf, REGBUF_WILL_INIT);
		XLogRegisterBuffer(1, fc_lbuf, REGBUF_STANDARD);
		XLogRegisterBuffer(2, fc_metabuf, REGBUF_WILL_INIT | REGBUF_STANDARD);

		Assert(fc_metad->btm_version >= BTREE_NOVAC_VERSION);
		fc_md.version = fc_metad->btm_version;
		fc_md.root = fc_rootblknum;
		fc_md.level = fc_metad->btm_level;
		fc_md.fastroot = fc_rootblknum;
		fc_md.fastlevel = fc_metad->btm_level;
		fc_md.last_cleanup_num_delpages = fc_metad->btm_last_cleanup_num_delpages;
		fc_md.allequalimage = fc_metad->btm_allequalimage;

		XLogRegisterBufData(2, (char *) &fc_md, sizeof(xl_btree_metadata));

		/*
		 * 直接访问页面不好，但更快——我们应该在页面API中
		 * 实现一些新功能。
		 */
		XLogRegisterBufData(0,
							(char *) fc_rootpage + ((PageHeader) fc_rootpage)->pd_upper,
							((PageHeader) fc_rootpage)->pd_special -
							((PageHeader) fc_rootpage)->pd_upper);

		fc_recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_NEWROOT);

		PageSetLSN(fc_lpage, fc_recptr);
		PageSetLSN(fc_rootpage, fc_recptr);
		PageSetLSN(fc_metapg, fc_recptr);
	}

	END_CRIT_SECTION();

	/* 与元数据页面处理完毕 */
	_bt_relbuf(fc_rel, fc_metabuf);

	pfree(fc_left_item);
	pfree(fc_right_item);

	return fc_rootbuf;
}

/*
 * _bt_pgaddtup() -- 在拆分过程中将数据项添加到特定页中。
 *
 * 这个例程与普通的 PageAddItem 调用的区别在于，这段代码可以处理内部 btree 页上的第一个数据项。
 * 这个数据项（在 _bt_split() 中称为 "firstright"）的键在拆分后必须视为负无穷。
 * 因此，当调用者指定这是分页上的第一个数据项时，我们将截断所有属性（不过下行指针不变）。
 * 这一额外步骤仅在内部页面拆分的右侧页面中是必要的。
 * 对于现有/左侧页面上的第一个数据项，则不需要这样做，因为在早期的页面拆分过程中已经被截断。
 *
 * 参见 _bt_split() 以获取我们在这里截断的高级解释。
 * 注意，这个例程与后缀截断无关，尽管使用了一些相同的基础设施。
 */
static inline bool fc__bt_pgaddtup(Page fc_page,
			 Size fc_itemsize,
			 IndexTuple fc_itup,
			 OffsetNumber fc_itup_off,
			 bool fc_newfirstdataitem)
{
	IndexTupleData fc_trunctuple;

	if (fc_newfirstdataitem)
	{
		fc_trunctuple = *fc_itup;
		fc_trunctuple.t_info = sizeof(IndexTupleData);
		BTreeTupleSetNAtts(&fc_trunctuple, 0, false);
		fc_itup = &fc_trunctuple;
		fc_itemsize = sizeof(IndexTupleData);
	}

	if (unlikely(PageAddItem(fc_page, (Item) fc_itup, fc_itemsize, fc_itup_off, false,
							 false) == InvalidOffsetNumber))
		return false;

	return true;
}

/*
 * _bt_delete_or_dedup_one_page - 尝试避免叶子页拆分。
 *
 * 这里执行了三种操作：简单索引删除、自下而上的索引删除和去重。
 * 如果这三种操作都无法为即将到来的项释放足够的空间，则调用者将继续拆分页面。
 * 我们总是优先考虑简单删除。如果那没有成功，我们会考虑其他替代方案。
 * 仅希望我们考虑简单删除（没有任何后备）的调用者可以通过 'simpleonly' 参数请求。
 *
 * 当简单删除单独无法防止页面拆分时，我们通常只选择一个替代的 "复杂" 操作。
 * 'checkingunique'、'uniquedup' 和 'indexUnchanged' 参数用于此目的。
 *
 * 注意：我们过去只在发现 BTP_HAS_GARBAGE 页面级标志已设置时删除 LP_DEAD 项。
 * 当时这个标志很有用，因为在版本 4 的索引中，重复元组并不一定有一个单一的页面（在 heap TID 成为键空间的一部分之前）。
 * 但我们实际上不再查看这个标志（它不是我们调用者的一个门控条件）。
 * 这会导致我们错过那些安全可删的元组，而没有获得任何回报。
 * 我们知道替代方案是拆分页面；在通过时扫描行指针数组并不会有显著的开销。
 * （尽管如此，我们仍然维护 BTP_HAS_GARBAGE 标志，因为 !heapkeyspace 索引仍然必须执行 "变得疲惫" 的线性搜索，
 * 所以很可能从将其作为门控条件中获得一些好处。）
 */
static void fc__bt_delete_or_dedup_one_page(Relation fc_rel, Relation fc_heapRel,
							 BTInsertState fc_insertstate,
							 bool fc_simpleonly, bool fc_checkingunique,
							 bool fc_uniquedup, bool fc_indexUnchanged)
{
	OffsetNumber fc_deletable[MaxIndexTuplesPerPage];
	int			fc_ndeletable = 0;
	OffsetNumber fc_offnum,
				fc_minoff,
				fc_maxoff;
	Buffer		fc_buffer = fc_insertstate->buf;
	BTScanInsert fc_itup_key = fc_insertstate->itup_key;
	Page		fc_page = BufferGetPage(fc_buffer);
	BTPageOpaque fc_opaque = BTPageGetOpaque(fc_page);

	Assert(P_ISLEAF(fc_opaque));
	Assert(fc_simpleonly || fc_itup_key->heapkeyspace);
	Assert(!fc_simpleonly || (!fc_checkingunique && !fc_uniquedup && !fc_indexUnchanged));

	/*
	 * 扫描所有项目，以查看根据 LP_DEAD 标志需要删除哪些项目。
	 * 我们通常会设法在此过程中删除一些未标记为 LP_DEAD 的额外项目。
	 * 通常，最终被删除的额外项目是那些不久后就会被标记为 LP_DEAD 的项目（如果我们选择不将它们包括作为额外项的话）。
	 */
	fc_minoff = P_FIRSTDATAKEY(fc_opaque);
	fc_maxoff = PageGetMaxOffsetNumber(fc_page);
	for (fc_offnum = fc_minoff;
		 fc_offnum <= fc_maxoff;
		 fc_offnum = OffsetNumberNext(fc_offnum))
	{
		ItemId		fc_itemId = PageGetItemId(fc_page, fc_offnum);

		if (ItemIdIsDead(fc_itemId))
			fc_deletable[fc_ndeletable++] = fc_offnum;
	}

	if (fc_ndeletable > 0)
	{
		fc__bt_simpledel_pass(fc_rel, fc_buffer, fc_heapRel, fc_deletable, fc_ndeletable,
						   fc_insertstate->itup, fc_minoff, fc_maxoff);
		fc_insertstate->bounds_valid = false;

		/* 返回时如果已经避免了页面分裂 */
		if (PageGetFreeSpace(fc_page) >= fc_insertstate->itemsz)
			return;

		/* 如果在检查唯一性时，可以假设存在重复项 */
		fc_uniquedup = true;
	}

	/*
	 * 我们完成了简单删除。在这里提前返回，仅适用于
	 * 几个只调用此处的调用者，以便可以考虑简单删除。
	 * 这包括明确要求此操作的调用者和可能在页面上没有任何版本更换重复项的检查唯一性调用者。
	 *
	 * 注意：当我们在此时返回时，页面的 BTP_HAS_GARBAGE 提示标志仍然可能被设置
	 * （或者当我们尝试其他策略时，这两者都失败）。然而，我们不会费心单独写入以清除它。
	 * 调用者在继续分裂页面时肯定会清除它（还要注意，去重过程会在经过时清除该标志，
	 * 只是为了保持整洁）。
	 */
	if (fc_simpleonly || (fc_checkingunique && !fc_uniquedup))
	{
		Assert(!fc_indexUnchanged);
		return;
	}

	/* 假设边界即将失效（这几乎是肯定的） */
	fc_insertstate->bounds_valid = false;

	/*
	 * 当执行器提示指示传入项在逻辑上未更改时，或者对于因某些其他原因已知具有物理重复项的唯一索引时，执行自下而上的索引删除。
	 * （对于唯一索引，这两种情况有很大的重叠。值得拥有这两种触发条件，以便在连续相关的 INSERT 和 DELETE 语句中应用优化。）
	 *
	 * 当自下而上的通过未能删除足够数量的空闲空间时，我们将继续进行去重通过，
	 * （页面的一定比例或新项所需空间，以较大者为准）。
	 *
	 * 注意：自下而上的索引删除在内部使用与去重相同的相等/等价例程。
	 * 然而，它并不会将索引元组合并在一起，因此相同的正确性考虑不适用。
	 * 我们故意在这里省略了索引是否完全相等的测试。
	 */
	if ((fc_indexUnchanged || fc_uniquedup) &&
		_bt_bottomupdel_pass(fc_rel, fc_buffer, fc_heapRel, fc_insertstate->itemsz))
		return;

	/* 执行去重过程（当启用并且索引完全相等时） */
	if (BTGetDeduplicateItems(fc_rel) && fc_itup_key->allequalimage)
		_bt_dedup_pass(fc_rel, fc_buffer, fc_heapRel, fc_insertstate->itup,
					   fc_insertstate->itemsz, (fc_indexUnchanged || fc_uniquedup));
}

/*
 * _bt_simpledel_pass - 简单索引元组删除过程。
 *
 * 我们删除叶子页面上所有标记为 LP_DEAD 的索引元组。所有这些元组的偏移量
 * 由调用者确定（调用者通过其“可删除”参数将这些元组传递给我们）。
 *
 * 我们也可能会在处理过程中删除额外的索引元组，这些元组被证明是安全删除的
 * （尽管它们在处理时必须检查便宜）。不过，不能确定会删除任何额外的元组。
 * 我们采用的这一方法的高层次目标是充分利用每一次调用
 * （而不会显著增加每次调用的开销，与我们为能够删除页面的 LP_DEAD 标记索引
 * 元组所需的相比）。
 *
 * 由于各种与局部性相关的影响，发现的可删除额外索引元组的数量可能
 * 大大超过标记为 LP_DEAD 的索引元组数量。例如，所有 TID 指向的
 * 表块（在叶子页面上）总数可能自然非常低，在这种情况下，我们可能会检查
 * 是否能删除页面上的大部分索引元组（而表访问管理器不需要访问额外的表块）。
 * 表访问管理器有时会在这种模式常见的索引中偶然发现大量额外的可删除索引元组。
 *
 * 有关简单索引元组删除的更多详细信息，请参见 nbtree/README。
 */
static void fc__bt_simpledel_pass(Relation fc_rel, Buffer fc_buffer, Relation fc_heapRel,
				   OffsetNumber *fc_deletable, int fc_ndeletable, IndexTuple fc_newitem,
				   OffsetNumber fc_minoff, OffsetNumber fc_maxoff)
{
	Page		fc_page = BufferGetPage(fc_buffer);
	BlockNumber *fc_deadblocks;
	int			fc_ndeadblocks;
	TM_IndexDeleteOp fc_delstate;
	OffsetNumber fc_offnum;

	/* 获取指向 LP_DEAD 集元组的表块数组 */
	fc_deadblocks = fc__bt_deadblocks(fc_page, fc_deletable, fc_ndeletable, fc_newitem,
								&fc_ndeadblocks);

	/* 初始化描述索引删除操作的表访问管理器状态 */
	fc_delstate.irel = fc_rel;
	fc_delstate.iblknum = BufferGetBlockNumber(fc_buffer);
	fc_delstate.bottomup = false;
	fc_delstate.bottomupfreespace = 0;
	fc_delstate.ndeltids = 0;
	fc_delstate.deltids = palloc(MaxTIDsPerBTreePage * sizeof(TM_IndexDelete));
	fc_delstate.status = palloc(MaxTIDsPerBTreePage * sizeof(TM_IndexStatus));

	for (fc_offnum = fc_minoff;
		 fc_offnum <= fc_maxoff;
		 fc_offnum = OffsetNumberNext(fc_offnum))
	{
		ItemId		fc_itemid = PageGetItemId(fc_page, fc_offnum);
		IndexTuple	fc_itup = (IndexTuple) PageGetItem(fc_page, fc_itemid);
		TM_IndexDelete *fc_odeltid = &fc_delstate.deltids[fc_delstate.ndeltids];
		TM_IndexStatus *fc_ostatus = &fc_delstate.status[fc_delstate.ndeltids];
		BlockNumber fc_tidblock;
		void	   *fc_match;

		if (!BTreeTupleIsPosting(fc_itup))
		{
			fc_tidblock = ItemPointerGetBlockNumber(&fc_itup->t_tid);
			fc_match = bsearch(&fc_tidblock, fc_deadblocks, fc_ndeadblocks,
							sizeof(BlockNumber), fc__bt_blk_cmp);

			if (!fc_match)
			{
				Assert(!ItemIdIsDead(fc_itemid));
				continue;
			}

			/*
			 * TID 的表块在页面上的 LP_DEAD 位集合元组的 TID 指向的
			 * 表块中 -- 将 TID 添加到 deltids
			 */
			fc_odeltid->tid = fc_itup->t_tid;
			fc_odeltid->id = fc_delstate.ndeltids;
			fc_ostatus->idxoffnum = fc_offnum;
			fc_ostatus->knowndeletable = ItemIdIsDead(fc_itemid);
			fc_ostatus->promising = false; /* 未使用 */
			fc_ostatus->freespace = 0; /* 未使用 */

			fc_delstate.ndeltids++;
		}
		else
		{
			int			fc_nitem = BTreeTupleGetNPosting(fc_itup);

			for (int fc_p = 0; fc_p < fc_nitem; fc_p++)
			{
				ItemPointer fc_tid = BTreeTupleGetPostingN(fc_itup, fc_p);

				fc_tidblock = ItemPointerGetBlockNumber(fc_tid);
				fc_match = bsearch(&fc_tidblock, fc_deadblocks, fc_ndeadblocks,
								sizeof(BlockNumber), fc__bt_blk_cmp);

				if (!fc_match)
				{
					Assert(!ItemIdIsDead(fc_itemid));
					continue;
				}

				/*
				 * TID 的表块在页面上的 LP_DEAD 位集合元组的 TID
				 * 指向的表块中 -- 将 TID 添加到 deltids
				 */
				fc_odeltid->tid = *fc_tid;
				fc_odeltid->id = fc_delstate.ndeltids;
				fc_ostatus->idxoffnum = fc_offnum;
				fc_ostatus->knowndeletable = ItemIdIsDead(fc_itemid);
				fc_ostatus->promising = false; /* 未使用 */
				fc_ostatus->freespace = 0; /* 未使用 */

				fc_odeltid++;
				fc_ostatus++;
				fc_delstate.ndeltids++;
			}
		}
	}

	pfree(fc_deadblocks);

	Assert(fc_delstate.ndeltids >= fc_ndeletable);

	/* 物理删除 LP_DEAD 元组（加上任何安全删除的额外 TID） */
	_bt_delitems_delete_check(fc_rel, fc_buffer, fc_heapRel, &fc_delstate);

	pfree(fc_delstate.deltids);
	pfree(fc_delstate.status);
}

/*
 * _bt_deadblocks() -- 获取与 LP_DEAD 相关的表块。
 *
 * 从标记为 LP_DEAD 的索引元组 TID 构建已排序和唯一化的表块编号数组。
 * 也会将来自输入的新元组的表块添加进来，以防它不在与 LP_DEAD 相关的
 * 表块中。
 *
 * 由于总是将新元组的表块视为与 LP_DEAD 相关的块是有意义的，因为成本
 * 一直很低；几乎可以肯定该表块不会在表访问管理器中引起缓冲区未命中。
 * 另一方面，收益通常相当高。因为一般而言，大多数垃圾元组在近期内
 * 变成垃圾，所以这个块中有一些可删除项的机会相当不错（在许多情况下
 * 这不会是核心代码最近添加到/修改的表块中的第一逻辑行）。
 *
 * 返回最终数组，并将 *nblocks 设置为调用者的最终大小。
 */
static BlockNumber *
fc__bt_deadblocks(Page fc_page, OffsetNumber *fc_deletable, int fc_ndeletable,
			   IndexTuple fc_newitem, int *fc_nblocks)
{
	int			fc_spacentids,
				fc_ntids;
	BlockNumber *fc_tidblocks;

	/*
	 * 按照数组的初始大小累积每个 TID 的块，该大小能够容纳
	 * 每个 LP_DEAD 集合元组的一个表块（加上新项表块的空间）。 
	 * 只有在有标记为 LP_DEAD 的贴文列表元组时，数组才需要增长
	 * （这并不常见）。
	 */
	fc_spacentids = fc_ndeletable + 1;
	fc_ntids = 0;
	fc_tidblocks = (BlockNumber *) palloc(sizeof(BlockNumber) * fc_spacentids);

	/*
	 * 首先为即将到来的新项添加表块。这是
	 * 唯一一个简单删除可以访问没有任何已知可删除项目的表块的情况。
	 */
	Assert(!BTreeTupleIsPosting(fc_newitem) && !BTreeTupleIsPivot(fc_newitem));
	fc_tidblocks[fc_ntids++] = ItemPointerGetBlockNumber(&fc_newitem->t_tid);

	for (int fc_i = 0; fc_i < fc_ndeletable; fc_i++)
	{
		ItemId		fc_itemid = PageGetItemId(fc_page, fc_deletable[fc_i]);
		IndexTuple	fc_itup = (IndexTuple) PageGetItem(fc_page, fc_itemid);

		Assert(ItemIdIsDead(fc_itemid));

		if (!BTreeTupleIsPosting(fc_itup))
		{
			if (fc_ntids + 1 > fc_spacentids)
			{
				fc_spacentids *= 2;
				fc_tidblocks = (BlockNumber *)
					repalloc(fc_tidblocks, sizeof(BlockNumber) * fc_spacentids);
			}

			fc_tidblocks[fc_ntids++] = ItemPointerGetBlockNumber(&fc_itup->t_tid);
		}
		else
		{
			int			fc_nposting = BTreeTupleGetNPosting(fc_itup);

			if (fc_ntids + fc_nposting > fc_spacentids)
			{
				fc_spacentids = Max(fc_spacentids * 2, fc_ntids + fc_nposting);
				fc_tidblocks = (BlockNumber *)
					repalloc(fc_tidblocks, sizeof(BlockNumber) * fc_spacentids);
			}

			for (int fc_j = 0; fc_j < fc_nposting; fc_j++)
			{
				ItemPointer fc_tid = BTreeTupleGetPostingN(fc_itup, fc_j);

				fc_tidblocks[fc_ntids++] = ItemPointerGetBlockNumber(fc_tid);
			}
		}
	}

	qsort(fc_tidblocks, fc_ntids, sizeof(BlockNumber), fc__bt_blk_cmp);
	*fc_nblocks = qunique(fc_tidblocks, fc_ntids, sizeof(BlockNumber), fc__bt_blk_cmp);

	return fc_tidblocks;
}

/*
 * _bt_blk_cmp() -- _bt_simpledel_pass 的 qsort 比较函数
 */
static inline int fc__bt_blk_cmp(const void *fc_arg1, const void *fc_arg2)
{
	BlockNumber fc_b1 = *((BlockNumber *) fc_arg1);
	BlockNumber fc_b2 = *((BlockNumber *) fc_arg2);

	if (fc_b1 < fc_b2)
		return -1;
	else if (fc_b1 > fc_b2)
		return 1;

	return 0;
}
